Repository: pydicom/pynetdicom

Issue:
not executable example in qr_move.rst
In the SCP part of the C-MOVE example it is unclear where the variable move_aet is defined. Is this event.move_aet ? The name also changes to move_ae later.

The name of the variable known_aet_dict also changes to known_ae_dict a few lines below.

Discussion:

Patch:
diff --git a/docs/examples/qr_move.rst b/docs/examples/qr_move.rst
index df2b3d6d7..c7de92256 100644
--- a/docs/examples/qr_move.rst
+++ b/docs/examples/qr_move.rst
@@ -187,18 +187,17 @@ to see the requirements for the ``evt.EVT_C_MOVE`` handler.
             yield 0xC000, None
             return
 
-        # Check move_aet is known
         # get_known_aet() is here to represent a user-implemented method of
-        #   getting known AEs
+        #   getting known AEs, for this example it returns a dict with the
+        #   AE titles as keys
         known_aet_dict = get_known_aet()
-        if move_aet not in known_aet_dict:
+        try:
+            (addr, port) = known_aet_dict[event.move_destination]
+        except KeyError:
             # Unknown destination AE
             yield (None, None)
             return
 
-        # Assuming known_ae_dict is {b'STORE_SCP       ' : ('127.0.0.1', 11113)}
-        (addr, port) = known_ae_dict[move_ae]
-
         # Yield the IP address and listen port of the destination AE
         yield (addr, port)
 
@@ -220,7 +219,7 @@ to see the requirements for the ``evt.EVT_C_MOVE`` handler.
         # Skip the other QR levels...
 
         # Yield the total number of C-STORE sub-operations required
-        yield len(instances)
+        yield len(matching)
 
         # Yield the matching instances
         for instance in matching:
diff --git a/pynetdicom/_handlers.py b/pynetdicom/_handlers.py
index 4e4a4c44c..6cbea52cf 100644
--- a/pynetdicom/_handlers.py
+++ b/pynetdicom/_handlers.py
@@ -2431,8 +2431,9 @@ def doc_handle_move(event):
           that is running the service that received the C-MOVE request.
         * ``context`` : the presentation context the request was sent under
           as a ``presentation.PresentationContextTuple``.
-        * ``description`` : a description of the event that occurred as str.
-        * ``name`` : the name of the event that occurred as str.
+        * ``description`` : a description of the event that occurred as
+          ``str``.
+        * ``name`` : the name of the event that occurred as ``str``.
         * ``request`` : the received
           :py:class:`C-MOVE request <pynetdicom.dimse_primitives.C_MOVE>`
         * ``timestamp`` : the
@@ -2447,9 +2448,11 @@ def doc_handle_move(event):
           a deferred read when decoding data, if the decode fails the returned
           ``Dataset`` will only raise an exception at the time of use.
         * ``is_cancelled`` : returns ``True`` if a
-          C-CANCEL request has been received, False otherwise. If a C-CANCEL
-          is received then the handler should yield a ``(0xFE00, None)``
-          status/dataset pair and return.
+          C-CANCEL request has been received, ``False`` otherwise. If a
+          C-CANCEL is received then the handler should yield a ``(0xFE00,
+          None)`` status/dataset pair and return.
+        * ``move_destination`` : returns the request's (0000,0600) *Move
+          Destination* AE title as a length 16 ``bytes``.
 
     Yields
     ------
diff --git a/pynetdicom/events.py b/pynetdicom/events.py
index e6fe66118..4b35f225b 100644
--- a/pynetdicom/events.py
+++ b/pynetdicom/events.py
@@ -253,8 +253,9 @@ class Event(object):
         Returns
         -------
         list of pydicom.tag.Tag
-            The *Attribute Identifier List* tags, may be an empty list if no
-            *Attribute Identifier List* was included in the C-GET request.
+            The (0000,1005) *Attribute Identifier List* tags, may be an empty
+            list if no *Attribute Identifier List* was included in the C-GET
+            request.
 
         Raises
         ------
@@ -549,6 +550,29 @@ class Event(object):
         )
         return self._get_dataset("ModificationList", msg)
 
+    @property
+    def move_destination(self):
+        """Return a C-MOVE request's `Move Destination` as bytes.
+
+        Returns
+        -------
+        bytes
+            The request's (0000,0600) *Move Destination* value as length 16
+            bytes (including trailing spaces as padding if required).
+
+        Raises
+        ------
+        AttributeError
+            If the corresponding event is not a C-MOVE request.
+        """
+        try:
+            return self.request.MoveDestination
+        except AttributeError:
+            raise AttributeError(
+                "The corresponding event is not a C-MOVE request and has no "
+                "'Move Destination' parameter"
+            )
+
     @property
     def name(self):
         """Return the name of the event as a str."""


Test patch:
diff --git a/pynetdicom/tests/test_events.py b/pynetdicom/tests/test_events.py
index 35bcb31e4..aee4f2d63 100644
--- a/pynetdicom/tests/test_events.py
+++ b/pynetdicom/tests/test_events.py
@@ -123,6 +123,13 @@ class TestEvent(object):
         with pytest.raises(AttributeError, match=msg):
             event.identifier
 
+        msg = (
+            r"The corresponding event is not a C-MOVE request "
+            r"and has no 'Move Destination' parameter"
+        )
+        with pytest.raises(AttributeError, match=msg):
+            event.move_destination
+
         msg = (
             r"The corresponding event is not an N-ACTION request and has no "
             r"'Action Information' parameter"
diff --git a/pynetdicom/tests/test_service_qr.py b/pynetdicom/tests/test_service_qr.py
index 655f690bd..089513ca7 100644
--- a/pynetdicom/tests/test_service_qr.py
+++ b/pynetdicom/tests/test_service_qr.py
@@ -4233,6 +4233,51 @@ class TestQRMoveServiceClass(object):
 
         scp.shutdown()
 
+    def test_scp_handler_move_dest(self):
+        """Test hander event's context attribute"""
+        attrs = {}
+        def handle(event):
+            attrs['request'] = event.request
+            attrs['identifier'] = event.identifier
+            attrs['assoc'] = event.assoc
+            attrs['context'] = event.context
+            attrs['destination'] = event.move_destination
+
+            yield self.destination
+            yield 1
+            yield 0xFF00, self.ds
+
+        def handle_store(event):
+            return 0x0000
+
+        handlers = [(evt.EVT_C_MOVE, handle), (evt.EVT_C_STORE, handle_store)]
+
+        self.ae = ae = AE()
+        ae.add_supported_context(PatientRootQueryRetrieveInformationModelMove)
+        ae.add_supported_context(CTImageStorage, scu_role=False, scp_role=True)
+        ae.add_requested_context(PatientRootQueryRetrieveInformationModelMove)
+        ae.add_requested_context(CTImageStorage)
+        scp = ae.start_server(('', 11112), block=False, evt_handlers=handlers)
+
+        ae.acse_timeout = 5
+        ae.dimse_timeout = 5
+        assoc = ae.associate('localhost', 11112)
+        assert assoc.is_established
+        result = assoc.send_c_move(self.query, b'TESTMOVE', query_model='P')
+        status, identifier = next(result)
+        assert status.Status == 0xFF00
+        assert identifier is None
+        status, identifier = next(result)
+        assert status.Status == 0x0000
+        assert identifier is None
+        pytest.raises(StopIteration, next, result)
+        assoc.release()
+        assert assoc.is_released
+
+        assert attrs['destination'] == b'TESTMOVE        '
+
+        scp.shutdown()
+
     def test_scp_cancelled(self):
         """Test is_cancelled works as expected."""
         cancel_results = []