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

795 Care about where a plan is sourced #807

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

callumforrester
Copy link
Contributor

Fixes #795

Copy link

codecov bot commented Jan 31, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 93.46%. Comparing base (e18b9be) to head (62f60b2).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #807   +/-   ##
=======================================
  Coverage   93.45%   93.46%           
=======================================
  Files          38       38           
  Lines        2108     2111    +3     
=======================================
+ Hits         1970     1973    +3     
  Misses        138      138           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@callumforrester callumforrester changed the title 795 plan sources 795 Care about where a plan is sourced Jan 31, 2025
The type annotations (e.g. `: str`, `: int`, `-> MsgGenerator`) are required as blueapi uses them to detect that this function is intended to be a plan and generate its runtime API.
## Detection

The type annotations in the example above (e.g. `: str`, `: int`, `-> MsgGenerator`) are required as blueapi uses them to detect that this function is intended to be a plan and generate its runtime API. If there is an [`__all__` dunder](https://docs.python.org/3/tutorial/modules.html#importing-from-a-package) present in the module, blueapi will read that and import anything within that qualifies as a plan, per its type annotations. If not it will simpyl read everything in the module that hasn't been imported, for example it will ignore a plan imported from another module.
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
The type annotations in the example above (e.g. `: str`, `: int`, `-> MsgGenerator`) are required as blueapi uses them to detect that this function is intended to be a plan and generate its runtime API. If there is an [`__all__` dunder](https://docs.python.org/3/tutorial/modules.html#importing-from-a-package) present in the module, blueapi will read that and import anything within that qualifies as a plan, per its type annotations. If not it will simpyl read everything in the module that hasn't been imported, for example it will ignore a plan imported from another module.
The type annotations in the example above (e.g. `: str`, `: int`, `-> MsgGenerator`) are required as blueapi uses them to detect that this function is intended to be a plan and generate its runtime API. If there is an [`__all__` dunder](https://docs.python.org/3/tutorial/modules.html#importing-from-a-package) present in the module, blueapi will use that: importing anything within that qualifies as a plan, per its type annotations. Else it will read everything in the module that hasn't been imported: e.g. ignoring a plan imported from another module.

Must simpyl -> simply or just being removed: I've decided to be opposed to adverbs in documentation- it doesn't matter if it is simply done or complexly done, only that it is done.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, master yoda

Comment on lines 111 to 114
if (
hasattr(module, "__all__")
or is_function_sourced_from_module(obj, module)
) and is_bluesky_plan_generator(obj):
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
if (
hasattr(module, "__all__")
or is_function_sourced_from_module(obj, module)
) and is_bluesky_plan_generator(obj):
if is_bluesky_plan_generator(obj) and (
hasattr(module, "__all__")
or is_function_sourced_from_module(obj, module)
):

Currently we import the module for every object that is found in the module- reversing this check will a. cut down on the number of times we try and import the module, and b. allow is_function_sourced_from_module to have tighter bounds.

@@ -34,3 +35,17 @@ def get_named_subset(names: list[str]):
for name, value in mod.__dict__.items():
if not name.startswith("_"):
yield value


def is_function_sourced_from_module(obj: Any, module: ModuleType) -> bool:
Copy link
Contributor

Choose a reason for hiding this comment

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

If not doing the above change of order of checking if obj is instance of Callable[[...], MsgGenerator], then this is (as suggested by the docstring) is_object_sourced_from_module...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Going with your first suggestion, I'm not aware of any constraint that guarantees that something being Callable means it has a __module__ dunder

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 just niting the name and the params telling different stories

@@ -170,6 +170,20 @@ def test_add_plan_from_module(empty_context: BlueskyContext) -> None:
assert EXPECTED_PLANS == empty_context.plans.keys()


def test_only_plans_from_source_module_detectede(empty_context: BlueskyContext) -> None:
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
def test_only_plans_from_source_module_detectede(empty_context: BlueskyContext) -> None:
def test_only_plans_from_source_module_detected(empty_context: BlueskyContext) -> None:

nit

assert {"plan_c", "plan_d"} == empty_context.plans.keys()


def test_only_plans_from_all_in_module_detectede(empty_context: BlueskyContext) -> None:
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
def test_only_plans_from_all_in_module_detectede(empty_context: BlueskyContext) -> None:
def test_only_plans_from_all_in_module_detected(empty_context: BlueskyContext) -> None:

nit

Comment on lines 49 to 51
return (
hasattr(obj, "__module__") and importlib.import_module(obj.__module__) is module
)
Copy link
Contributor

Choose a reason for hiding this comment

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

When this is being used over all of the objects in a module, is it going to import the module and run any side effects for every object? This could (in plan modules out of our control) be dangerous. Or does it cache and only import the module once?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It caches

# tt.py
print("Hello world")
>>> import importlib
>>> importlib.import_module("tt")
Hello world
<module 'tt' from '/workspaces/blueapi/tt.py'>
>>> importlib.import_module("tt")
<module 'tt' from '/workspaces/blueapi/tt.py'>

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.

Importing attach_data_session_metadata_decorator means it tries to export as a plan
2 participants