-
Notifications
You must be signed in to change notification settings - Fork 6
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
base: main
Are you sure you want to change the base?
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
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. |
docs/how-to/write-plans.md
Outdated
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. |
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 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.
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.
Yes, master yoda
src/blueapi/core/context.py
Outdated
if ( | ||
hasattr(module, "__all__") | ||
or is_function_sourced_from_module(obj, module) | ||
) and is_bluesky_plan_generator(obj): |
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.
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.
src/blueapi/utils/modules.py
Outdated
@@ -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: |
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.
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
...)
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.
Going with your first suggestion, I'm not aware of any constraint that guarantees that something being Callable
means it has a __module__
dunder
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'm just nit
ing 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: |
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.
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: |
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.
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
src/blueapi/utils/modules.py
Outdated
return ( | ||
hasattr(obj, "__module__") and importlib.import_module(obj.__module__) is module | ||
) |
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.
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?
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.
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'>
Fixes #795