Repository: pydicom/pynetdicom

Issue:
C-store  image  Fail

**Describe the bug**
C-STORE image  Fail, but dcmtk success
error:
```
2023-11-24 20:03:15,916 - [ThreadID:16820] - [FuncName:encode]-[Line:166] - ERROR: With tag (0008, 0008) got exception: encoding without a string argument
Traceback (most recent call last):
  File "E:\Python37\lib\site-packages\pydicom\tag.py", line 28, in tag_in_exception
    yield
  File "E:\Python37\lib\site-packages\pydicom\filewriter.py", line 660, in write_dataset
    write_data_element(fp, dataset.get_item(tag), dataset_encoding)
  File "E:\Python37\lib\site-packages\pydicom\filewriter.py", line 604, in write_data_element
    fp.write(bytes(vr, default_encoding))
TypeError: encoding without a string argument
Traceback (most recent call last):
  File "E:\Python37\lib\site-packages\pydicom\tag.py", line 28, in tag_in_exception
    yield
  File "E:\Python37\lib\site-packages\pydicom\filewriter.py", line 660, in write_dataset
    write_data_element(fp, dataset.get_item(tag), dataset_encoding)
  File "E:\Python37\lib\site-packages\pydicom\filewriter.py", line 604, in write_data_element
    fp.write(bytes(vr, default_encoding))
TypeError: encoding without a string argument

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "E:\Python37\lib\site-packages\pynetdicom\dsutils.py", line 163, in encode
    write_dataset(fp, ds)
  File "E:\Python37\lib\site-packages\pydicom\filewriter.py", line 660, in write_dataset
    write_data_element(fp, dataset.get_item(tag), dataset_encoding)
  File "E:\Python37\lib\contextlib.py", line 130, in __exit__
    self.gen.throw(type, value, traceback)
  File "E:\Python37\lib\site-packages\pydicom\tag.py", line 32, in tag_in_exception
    raise type(exc)(msg) from exc
TypeError: With tag (0008, 0008) got exception: encoding without a string argument
Traceback (most recent call last):
  File "E:\Python37\lib\site-packages\pydicom\tag.py", line 28, in tag_in_exception
    yield
  File "E:\Python37\lib\site-packages\pydicom\filewriter.py", line 660, in write_dataset
    write_data_element(fp, dataset.get_item(tag), dataset_encoding)
  File "E:\Python37\lib\site-packages\pydicom\filewriter.py", line 604, in write_data_element
    fp.write(bytes(vr, default_encoding))
TypeError: encoding without a string argument
```

**Expected behavior**
What you expected to happen (please include a reference to the DICOM standard if relevant).

**Steps To Reproduce**
How to reproduce the issue. Please include a minimum working code sample, the relevant section of the logging output at the debug level (from `pynetdicom import debug_logger; debug_logger()`) and the anonymised DICOM dataset (if relevant).

**Your environment**
Please run the following and paste the output.
```bash
$ python -c "import platform; print(platform.platform())"
$ python -c "import sys; print('Python ', sys.version)"
$ python -c "import pydicom; print('pydicom ', pydicom.__version__)"
$ python -c "import pynetdicom; print('pynetdicom ', pynetdicom.__version__)"
```


Discussion:

Patch:
diff --git a/docs/changelog/v2.1.0.rst b/docs/changelog/v2.1.0.rst
index 060010a6c..6394688f5 100644
--- a/docs/changelog/v2.1.0.rst
+++ b/docs/changelog/v2.1.0.rst
@@ -13,6 +13,7 @@ Fixes
 * Sanitise filenames for received datasets for non-conformant SOP Instance
   UIDs (:issue:`823`)
 
+
 Enhancements
 ............
 
@@ -27,6 +28,10 @@ Enhancements
   <pynetdicom.service_class.StorageManagementServiceClass>` (:issue:`880`)
 * Added :meth:`~pynetdicom.events.Event.encoded_dataset` to simplify accessing
   the encoded dataset without first decoding it
+* Added a check to :meth:`~pynetdicom.association.Association.send_c_store` to
+  ensure that the *Transfer Syntax UID* matches the encoding of the dataset
+  (:issue:`891`)
+
 
 Changes
 .......
diff --git a/pynetdicom/ae.py b/pynetdicom/ae.py
index eb3983f4d..f7fc0a6f6 100644
--- a/pynetdicom/ae.py
+++ b/pynetdicom/ae.py
@@ -45,8 +45,8 @@ LOGGER = logging.getLogger("pynetdicom.ae")
 
 
 _T = TypeVar("_T")
-ListCXType = List[PresentationContext]
-TSyntaxType = Optional[Union[str, UID, Sequence[Union[str, UID]]]]
+ListCXType = list[PresentationContext]
+TSyntaxType = None | str | UID | Sequence[str] | Sequence[UID]
 
 
 class ApplicationEntity:
diff --git a/pynetdicom/association.py b/pynetdicom/association.py
index 48027cec8..bb4390aa2 100644
--- a/pynetdicom/association.py
+++ b/pynetdicom/association.py
@@ -18,7 +18,7 @@ import warnings
 from pydicom import dcmread
 from pydicom.dataset import Dataset
 from pydicom.tag import BaseTag
-from pydicom.uid import UID
+from pydicom.uid import UID, ImplicitVRLittleEndian, ExplicitVRBigEndian
 
 # pylint: disable=no-name-in-module
 from pynetdicom.acse import ACSE
@@ -1881,6 +1881,34 @@ class Association(threading.Thread):
                     "UID' file meta information element"
                 )
 
+            ts_encoding: tuple[bool, bool] = (
+                tsyntax.is_implicit_VR,
+                tsyntax.is_little_endian,
+            )
+            # `dataset` might also be created from scratch
+            ds_encoding = getattr(
+                dataset,
+                "original_encoding",
+                (dataset.is_implicit_VR, dataset.is_little_endian),
+            )
+            if None not in ds_encoding and ts_encoding != ds_encoding:
+                s = ("explicit VR", "implicit VR")[cast(bool, ds_encoding[0])]
+                s += (" big endian", " little endian")[cast(bool, ds_encoding[1])]
+                msg = (
+                    f"'dataset' is encoded as {s} but the file meta has a "
+                    f"(0002,0010) Transfer Syntax UID of '{tsyntax.name}'"
+                )
+                if ds_encoding == (True, True):
+                    LOGGER.warning(f"{msg} - using 'Implicit VR Little Endian' instead")
+                    tsyntax = ImplicitVRLittleEndian
+                elif ds_encoding == (False, False):
+                    LOGGER.warning(f"{msg} - using 'Explicit VR Big Endian' instead")
+                    tsyntax = ExplicitVRBigEndian
+                else:
+                    raise AttributeError(
+                        f"{msg} - please set an appropriate Transfer Syntax"
+                    )
+
         # Get a Presentation Context to use for sending the message
         context = self._get_valid_context(
             sop_class, tsyntax, "scu", allow_conversion=allow_conversion


Test patch:
diff --git a/pynetdicom/tests/test_assoc.py b/pynetdicom/tests/test_assoc.py
index f635648ef..0d2fa22d8 100644
--- a/pynetdicom/tests/test_assoc.py
+++ b/pynetdicom/tests/test_assoc.py
@@ -1846,6 +1846,69 @@ class TestAssociationSendCStore:
 
         scp.shutdown()
 
+    def test_dataset_encoding_mismatch(self, caplog):
+        """Tests for when transfer syntax doesn't match dataset encoding."""
+
+        def handle_store(event):
+            return 0x0000
+
+        handlers = [(evt.EVT_C_STORE, handle_store)]
+
+        self.ae = ae = AE()
+        ae.acse_timeout = 5
+        ae.dimse_timeout = 5
+        ae.network_timeout = 5
+        ae.add_supported_context(
+            CTImageStorage,
+            [ExplicitVRBigEndian, ImplicitVRLittleEndian],
+        )
+        scp = ae.start_server(("localhost", 11112), block=False, evt_handlers=handlers)
+
+        ae.add_requested_context(CTImageStorage, ImplicitVRLittleEndian)
+        ae.add_requested_context(CTImageStorage, ExplicitVRBigEndian)
+        assoc = ae.associate("localhost", 11112)
+
+        assert assoc.is_established
+        ds = dcmread(DATASET_PATH)
+        assert ds.is_little_endian
+        assert not ds.is_implicit_VR
+        assert ds.file_meta.TransferSyntaxUID == ExplicitVRLittleEndian
+        ds.is_implicit_VR = True
+        with caplog.at_level(logging.WARNING, logger="pynetdicom"):
+            status = assoc.send_c_store(ds)
+            assert status.Status == 0x0000
+
+            ds.is_implicit_VR = False
+            ds.is_little_endian = False
+            status = assoc.send_c_store(ds)
+            assert status.Status == 0x0000
+
+        ds.is_implicit_VR = False
+        ds.is_little_endian = True
+        ds.file_meta.TransferSyntaxUID = ImplicitVRLittleEndian
+        msg = (
+            "'dataset' is encoded as explicit VR little endian but the file "
+            r"meta has a \(0002,0010\) Transfer Syntax UID of 'Implicit VR "
+            "Little Endian' - please set an appropriate Transfer Syntax"
+        )
+        with pytest.raises(AttributeError, match=msg):
+            status = assoc.send_c_store(ds)
+
+        assoc.release()
+        assert assoc.is_released
+        scp.shutdown()
+
+        assert (
+            "'dataset' is encoded as implicit VR little endian but the file "
+            "meta has a (0002,0010) Transfer Syntax UID of 'Explicit VR "
+            "Little Endian' - using 'Implicit VR Little Endian' instead"
+        ) in caplog.text
+        assert (
+            "'dataset' is encoded as explicit VR big endian but the file "
+            "meta has a (0002,0010) Transfer Syntax UID of 'Explicit VR "
+            "Little Endian' - using 'Explicit VR Big Endian' instead"
+        ) in caplog.text
+
     # Regression tests
     def test_no_send_mismatch(self):
         """Test sending a dataset with mismatched transfer syntax (206)."""