Repository: bluesky/bluesky (python)

Issue:
Add bps.locate plan stub
We have a `Locate` message, but not a corresponding plan stub. Something like:
```python
@typing.overload
def locate(obj: Locatable, squeeze: Literal[True] = True) -> Location: ...
@typing.overload
def locate(*objs: Locatable, squeeze: bool = True) -> List[Location]: ...

def locate(*objs: Locatable, squeeze=True) -> List[Location]:
    return Msg("locate", *objs, squeeze=squeeze)
```
with the docstring from https://github.com/bluesky/bluesky/blob/main/src/bluesky/run_engine.py#L1959


Pull request description:
Add bps.locate to plan_stub
Closes #1753 

Added `Locate` to `bps` since was already in `RunEngine` (but not `bps`)

Patch:
diff --git a/src/bluesky/plan_stubs.py b/src/bluesky/plan_stubs.py
index 3cd56bec..8d24b627 100644
--- a/src/bluesky/plan_stubs.py
+++ b/src/bluesky/plan_stubs.py
@@ -1,11 +1,12 @@
 import itertools
 import operator
 import time
+import typing
 import uuid
 import warnings
 from collections.abc import Iterable
 from functools import reduce
-from typing import List
+from typing import List, Literal
 
 from cycler import cycler
 
@@ -15,7 +16,7 @@ try:
 except ImportError:
     from toolz import partition
 
-from .protocols import Flyable, Locatable, Status, Triggerable, check_supports
+from .protocols import Flyable, Locatable, Location, Status, Triggerable, check_supports
 from .utils import (
     Msg,
     all_safe_rewind,
@@ -131,6 +132,30 @@ def read(obj):
     return (yield Msg("read", obj))
 
 
+@typing.overload
+def locate(obj: Locatable, squeeze: Literal[True] = True) -> Location: ...  # type: ignore[overload-overlap]
+@typing.overload
+def locate(*objs: Locatable, squeeze: bool = True) -> List[Location]: ...
+@plan
+def locate(*objs, squeeze=True):
+    """
+    Locate some Movables and return their locations.
+
+    Parameters
+    ----------
+    obj : Device or Signal
+    sqeeze: bool
+        If True, return the result as a list.
+        If False, always return a list of retults even with a single object.
+
+    Yields
+    ------
+     msg : Msg
+        ``Msg('locate', obj1, ..., objn, squeeze=True)``
+    """
+    return (yield Msg("locate", *objs, squeeze=squeeze))
+
+
 @plan
 def monitor(obj, *, name=None, **kwargs):
     """


Test patch:
diff --git a/src/bluesky/tests/test_new_examples.py b/src/bluesky/tests/test_new_examples.py
index f7115f78..8bb846d5 100644
--- a/src/bluesky/tests/test_new_examples.py
+++ b/src/bluesky/tests/test_new_examples.py
@@ -21,6 +21,7 @@ from bluesky.plan_stubs import (
     drop,
     install_suspender,
     kickoff,
+    locate,
     monitor,
     mv,
     mvr,
@@ -80,6 +81,13 @@ from bluesky.utils import IllegalMessageSequence, all_safe_rewind
         (save, (), {}, [Msg("save")]),
         (drop, (), {}, [Msg("drop")]),
         (read, ("det",), {}, [Msg("read", "det")]),
+        (locate, (), {}, [Msg("locate", squeeze=True)]),
+        (
+            locate,
+            ("foo",),
+            {"squeeze": False},
+            [Msg("locate", "foo", squeeze=False)],
+        ),
         (monitor, ("foo",), {}, [Msg("monitor", "foo", name=None)]),
         (monitor, ("foo",), {"name": "c"}, [Msg("monitor", "foo", name="c")]),
         (unmonitor, ("foo",), {}, [Msg("unmonitor", "foo")]),