Repository: pydicom/pydicom

Issue:
Flaky tests
**Describe the bug**
`pydicom/tests/test_dataelem.py::TestRawDataElement::test_read_known_private_tag_implicit` and `pydicom/tests/test_dataelem.py::TestRawDataElement::test_read_known_private_tag_explicit` can fail when running for multiple times.

**Expected behavior**
All testcases pass.

**Steps To Reproduce**
`pip install pytest-flakefinder`
`pytest --flake-finder --flake-runs=2`

**Your environment**
```
module       | version
------       | -------
platform     | Linux-4.15.0-161-generic-x86_64-with-Ubuntu-18.04-bionic
Python       | 3.7.12 (default, Sep 10 2021, 00:21:48)  [GCC 7.5.0]
pydicom      | 2.3.0.dev0
gdcm         | _module not found_
jpeg_ls      | _module not found_
numpy        | _module not found_
PIL          | _module not found_
pylibjpeg    | _module not found_
openjpeg     | _module not found_
libjpeg      | _module not found_
```

Discussion:
pep8speaks: Hello @mrbean-bremen! Thanks for opening this PR. We checked the lines you've touched for [PEP 8](https://www.python.org/dev/peps/pep-0008) issues, and found:

* In the file [`pydicom/tests/test_dataelem.py`](https://github.com/pydicom/pydicom/blob/5f730c91b8e8873fc55d67824244b05d8264b88f/pydicom/tests/test_dataelem.py):

> [Line 708:80](https://github.com/pydicom/pydicom/blob/5f730c91b8e8873fc55d67824244b05d8264b88f/pydicom/tests/test_dataelem.py#L708): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (82 > 79 characters)

* In the file [`pydicom/tests/test_dictionary.py`](https://github.com/pydicom/pydicom/blob/5f730c91b8e8873fc55d67824244b05d8264b88f/pydicom/tests/test_dictionary.py):

> [Line 100:80](https://github.com/pydicom/pydicom/blob/5f730c91b8e8873fc55d67824244b05d8264b88f/pydicom/tests/test_dictionary.py#L100): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (81 > 79 characters)



* In the file [`pydicom/tests/test_util.py`](https://github.com/pydicom/pydicom/blob/5f730c91b8e8873fc55d67824244b05d8264b88f/pydicom/tests/test_util.py):

> [Line 212:80](https://github.com/pydicom/pydicom/blob/5f730c91b8e8873fc55d67824244b05d8264b88f/pydicom/tests/test_util.py#L212): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (82 > 79 characters)
> [Line 228:80](https://github.com/pydicom/pydicom/blob/5f730c91b8e8873fc55d67824244b05d8264b88f/pydicom/tests/test_util.py#L228): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (82 > 79 characters)
> [Line 264:80](https://github.com/pydicom/pydicom/blob/5f730c91b8e8873fc55d67824244b05d8264b88f/pydicom/tests/test_util.py#L264): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (86 > 79 characters)
> [Line 270:80](https://github.com/pydicom/pydicom/blob/5f730c91b8e8873fc55d67824244b05d8264b88f/pydicom/tests/test_util.py#L270): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (82 > 79 characters)
> [Line 273:80](https://github.com/pydicom/pydicom/blob/5f730c91b8e8873fc55d67824244b05d8264b88f/pydicom/tests/test_util.py#L273): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (83 > 79 characters)




codecov[bot]: # [Codecov](https://codecov.io/gh/pydicom/pydicom/pull/1541?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) Report
> Merging [#1541](https://codecov.io/gh/pydicom/pydicom/pull/1541?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) (5f730c9) into [master](https://codecov.io/gh/pydicom/pydicom/commit/a125a02132c2db5ff5cad445e4722802dd5a8d55?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) (a125a02) will **not change** coverage.
> The diff coverage is `n/a`.

> :exclamation: Current head 5f730c9 differs from pull request most recent head 2748074. Consider uploading reports for the commit 2748074 to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/pydicom/pydicom/pull/1541/graphs/tree.svg?width=650&height=150&src=pr&token=YciLV15Lwr&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom)](https://codecov.io/gh/pydicom/pydicom/pull/1541?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom)

```diff
@@           Coverage Diff           @@
##           master    #1541   +/-   ##
=======================================
  Coverage   97.42%   97.42%           
=======================================
  Files          66       66           
  Lines       10182    10182           
=======================================
  Hits         9920     9920           
  Misses        262      262           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/pydicom/pydicom/pull/1541?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pydicom/pydicom/pull/1541?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom). Last update [a125a02...2748074](https://codecov.io/gh/pydicom/pydicom/pull/1541?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).


Patch:
diff --git a/doc/release_notes/index.rst b/doc/release_notes/index.rst
index 85f5be99c..4d47c7285 100644
--- a/doc/release_notes/index.rst
+++ b/doc/release_notes/index.rst
@@ -2,6 +2,7 @@
 Release notes
 =============
 
+.. include:: v2.3.0.rst
 .. include:: v2.2.0.rst
 .. include:: v2.1.1.rst
 .. include:: v2.1.0.rst
diff --git a/doc/release_notes/v2.3.0.rst b/doc/release_notes/v2.3.0.rst
new file mode 100644
index 000000000..743f24e81
--- /dev/null
+++ b/doc/release_notes/v2.3.0.rst
@@ -0,0 +1,13 @@
+Version 2.3.0
+=================================
+
+Changes
+-------
+
+Enhancements
+------------
+
+Fixes
+-----
+
+* Fixed odd-length **OB** values not being padded during write (:issue:`1511`)
diff --git a/pydicom/filewriter.py b/pydicom/filewriter.py
index 6f97b0e75..ab164a61b 100644
--- a/pydicom/filewriter.py
+++ b/pydicom/filewriter.py
@@ -272,7 +272,12 @@ def write_numbers(fp: DicomIO, elem: DataElement, struct_format: str) -> None:
 
 def write_OBvalue(fp: DicomIO, elem: DataElement) -> None:
     """Write a data_element with VR of 'other byte' (OB)."""
-    fp.write(cast(bytes, elem.value))
+    if len(elem.value) % 2:
+        # Pad odd length values
+        fp.write(cast(bytes, elem.value))
+        fp.write(b'\x00')
+    else:
+        fp.write(cast(bytes, elem.value))
 
 
 def write_OWvalue(fp: DicomIO, elem: DataElement) -> None:


Test patch:
diff --git a/pydicom/tests/test_dataelem.py b/pydicom/tests/test_dataelem.py
index 8fb15aeed..da5d7daf1 100644
--- a/pydicom/tests/test_dataelem.py
+++ b/pydicom/tests/test_dataelem.py
@@ -21,6 +21,7 @@ from pydicom.errors import BytesLengthException
 from pydicom.filebase import DicomBytesIO
 from pydicom.multival import MultiValue
 from pydicom.tag import Tag, BaseTag
+from pydicom.tests.test_util import save_private_dict
 from pydicom.uid import UID
 from pydicom.valuerep import DSfloat
 
@@ -663,18 +664,19 @@ class TestRawDataElement:
         assert elem.name == "Private tag data"
         assert elem.value == b"\x2A\x00"
 
-        add_private_dict_entry("ACME 3.2", 0x00410001, "US", "Some Number")
-        ds = dcmread(fp, force=True)
-        elem = ds[0x00411001]
-        assert elem.VR == "US"
-        assert elem.name == "[Some Number]"
-        assert elem.value == 42
+        with save_private_dict():
+            add_private_dict_entry("ACME 3.2", 0x00410001, "US", "Some Number")
+            ds = dcmread(fp, force=True)
+            elem = ds[0x00411001]
+            assert elem.VR == "US"
+            assert elem.name == "[Some Number]"
+            assert elem.value == 42
 
-        # Unknown private tag is handled as before
-        elem = ds[0x00431001]
-        assert elem.VR == "UN"
-        assert elem.name == "Private tag data"
-        assert elem.value == b"Unknown "
+            # Unknown private tag is handled as before
+            elem = ds[0x00431001]
+            assert elem.VR == "UN"
+            assert elem.name == "Private tag data"
+            assert elem.value == b"Unknown "
 
     def test_read_known_private_tag_explicit(self):
         fp = DicomBytesIO()
@@ -692,27 +694,30 @@ class TestRawDataElement:
         assert elem.name == "Private tag data"
         assert elem.value == b"SOME_AET"
 
-        add_private_dict_entry("ACME 3.2", 0x00410002, "AE", "Some AET")
-        ds = dcmread(fp, force=True)
-        elem = ds[0x00411002]
-        assert elem.VR == "AE"
-        assert elem.name == "[Some AET]"
-        assert elem.value == "SOME_AET"
+        with save_private_dict():
+            add_private_dict_entry("ACME 3.2", 0x00410002, "AE", "Some AET")
+            ds = dcmread(fp, force=True)
+            elem = ds[0x00411002]
+            assert elem.VR == "AE"
+            assert elem.name == "[Some AET]"
+            assert elem.value == "SOME_AET"
 
     def test_read_known_private_tag_explicit_no_lookup(
             self, dont_replace_un_with_known_vr):
-        add_private_dict_entry("ACME 3.2", 0x00410003, "IS", "Another Number")
-        fp = DicomBytesIO()
-        ds = Dataset()
-        ds.is_implicit_VR = False
-        ds.is_little_endian = True
-        ds[0x00410010] = RawDataElement(
-            Tag(0x00410010), "LO", 8, b"ACME 3.2", 0, False, True)
-        ds[0x00411003] = RawDataElement(
-            Tag(0x00411003), "UN", 8, b"12345678", 0, False, True)
-        ds.save_as(fp)
-        ds = dcmread(fp, force=True)
-        elem = ds[0x00411003]
-        assert elem.VR == "UN"
-        assert elem.name == "[Another Number]"
-        assert elem.value == b"12345678"
+        with save_private_dict():
+            add_private_dict_entry(
+                "ACME 3.2", 0x00410003, "IS", "Another Number")
+            fp = DicomBytesIO()
+            ds = Dataset()
+            ds.is_implicit_VR = False
+            ds.is_little_endian = True
+            ds[0x00410010] = RawDataElement(
+                Tag(0x00410010), "LO", 8, b"ACME 3.2", 0, False, True)
+            ds[0x00411003] = RawDataElement(
+                Tag(0x00411003), "UN", 8, b"12345678", 0, False, True)
+            ds.save_as(fp)
+            ds = dcmread(fp, force=True)
+            elem = ds[0x00411003]
+            assert elem.VR == "UN"
+            assert elem.name == "[Another Number]"
+            assert elem.value == b"12345678"
diff --git a/pydicom/tests/test_dictionary.py b/pydicom/tests/test_dictionary.py
index 4f183a27c..30af2de7e 100644
--- a/pydicom/tests/test_dictionary.py
+++ b/pydicom/tests/test_dictionary.py
@@ -12,6 +12,7 @@ from pydicom.datadict import (keyword_for_tag, dictionary_description,
                               private_dictionary_VM, add_private_dict_entries,
                               add_private_dict_entry)
 from pydicom.datadict import add_dict_entry, add_dict_entries
+from pydicom.tests.test_util import save_private_dict
 
 
 class TestDict:
@@ -95,11 +96,13 @@ class TestDict:
             add_dict_entries(new_dict_items)
 
     def test_add_private_entry(self):
-        add_private_dict_entry('ACME 3.1', 0x10011101, 'DS', 'Test One', '3')
-        entry = get_private_entry((0x1001, 0x0001), 'ACME 3.1')
-        assert 'DS' == entry[0]  # VR
-        assert '3' == entry[1]  # VM
-        assert 'Test One' == entry[2]  # description
+        with save_private_dict():
+            add_private_dict_entry(
+                'ACME 3.1', 0x10011101, 'DS', 'Test One', '3')
+            entry = get_private_entry((0x1001, 0x0001), 'ACME 3.1')
+            assert 'DS' == entry[0]  # VR
+            assert '3' == entry[1]  # VM
+            assert 'Test One' == entry[2]  # description
 
     def test_add_private_entry_raises_for_non_private_tag(self):
         msg = (
diff --git a/pydicom/tests/test_filewriter.py b/pydicom/tests/test_filewriter.py
index 099d56307..dea21a238 100644
--- a/pydicom/tests/test_filewriter.py
+++ b/pydicom/tests/test_filewriter.py
@@ -474,6 +474,20 @@ class TestWriteDataElement:
         data_elem = DataElement(0x00080060, 'CS', b'REG')
         self.check_data_element(data_elem, expected)
 
+    def test_write_OB_odd(self):
+        """Test an odd-length OB element is padded during write"""
+        value = b'\x00\x01\x02'
+        elem = DataElement(0x7FE00010, 'OB', value)
+        encoded_elem = self.encode_element(elem)
+        ref_bytes = b'\xe0\x7f\x10\x00\x04\x00\x00\x00' + value + b"\x00"
+        assert ref_bytes == encoded_elem
+
+        # Empty data
+        elem.value = b''
+        encoded_elem = self.encode_element(elem)
+        ref_bytes = b'\xe0\x7f\x10\x00\x00\x00\x00\x00'
+        assert ref_bytes == encoded_elem
+
     def test_write_OD_implicit_little(self):
         """Test writing elements with VR of OD works correctly."""
         # VolumetricCurvePoints
diff --git a/pydicom/tests/test_overlay_np.py b/pydicom/tests/test_overlay_np.py
index 1eb5b4ddd..47211ebaf 100644
--- a/pydicom/tests/test_overlay_np.py
+++ b/pydicom/tests/test_overlay_np.py
@@ -201,13 +201,6 @@ class TestNumpy_NumpyHandler:
         assert arr.min() == 0
         assert 29 == sum(arr[422, 393:422])
 
-    def test_typing_imports(self, monkeypatch):
-        """Test the imports required for typing are OK."""
-        assert not hasattr(NP_HANDLER, "Dataset")
-        monkeypatch.setattr(typing, "TYPE_CHECKING", True)
-        reload(NP_HANDLER)
-        assert hasattr(NP_HANDLER, "Dataset")
-
 
 # Tests for numpy_handler module with Numpy available
 @pytest.mark.skipif(not HAVE_NP, reason='Numpy is not available')
diff --git a/pydicom/tests/test_util.py b/pydicom/tests/test_util.py
index 196dc8d29..115da1fd3 100644
--- a/pydicom/tests/test_util.py
+++ b/pydicom/tests/test_util.py
@@ -1,14 +1,13 @@
 # Copyright 2008-2021 pydicom authors. See LICENSE file for details.
 """Test suite for util functions"""
-
-from io import BytesIO
-import os
+import copy
+from contextlib import contextmanager
 
 import pytest
 
 from pydicom import config, dcmread
 from pydicom import filereader
-from pydicom import valuerep
+from pydicom._private_dict import private_dictionaries
 from pydicom.data import get_testdata_file
 from pydicom.dataelem import DataElement
 from pydicom.dataset import Dataset
@@ -30,7 +29,6 @@ from pydicom.util.dump import *
 from pydicom.util.hexutil import hex2bytes, bytes2hex
 from pydicom.util.leanread import dicomfile
 
-
 have_numpy = True
 try:
     import numpy
@@ -434,3 +432,13 @@ class TestLeanRead:
             with pytest.raises(NotImplementedError, match=msg):
                 for elem in ds:
                     pass
+
+
+@contextmanager
+def save_private_dict():
+    saved_private_dict = copy.deepcopy(private_dictionaries)
+    try:
+        yield
+    finally:
+        private_dictionaries.clear()
+        private_dictionaries.update(saved_private_dict)
diff --git a/pydicom/tests/test_waveform.py b/pydicom/tests/test_waveform.py
index cd86f4616..1c8e6955b 100644
--- a/pydicom/tests/test_waveform.py
+++ b/pydicom/tests/test_waveform.py
@@ -44,15 +44,6 @@ def test_simple():
     arr = ds.waveform_array(index=1)
 
 
-@pytest.mark.skipif(not HAVE_NP, reason="Numpy not available")
-def test_typing_imports(monkeypatch):
-    """Test the imports required for typing are OK."""
-    assert not hasattr(NP_HANDLER, "Dataset")
-    monkeypatch.setattr(typing, "TYPE_CHECKING", True)
-    reload(NP_HANDLER)
-    assert hasattr(NP_HANDLER, "Dataset")
-
-
 @pytest.mark.skipif(not HAVE_NP, reason="Numpy not available")
 class TestHandlerGenerateMultiplex:
     """Tests for the waveform numpy_handler.generate_multiplex."""