Repository: pydicom/pydicom

Issue:
Print byte values for unknown VR during read
#### Description
If the dataset read fails due to an unknown VR then the exception message prints the VR bytes in a format that isn't useful for debugging.

#### Steps/Code to Reproduce
```python
from io import BytesIO
from pydicom.filereader import read_dataset
ds = read_dataset(BytesIO(b'\x08\x00\x01\x00\x04\x00\x00\x00\x00\x08\x00\x49'), False, True)
print(ds)
```

#### Expected Results
```
NotImplementedError: Unknown Value Representation: '32 31' in tag (0000, 0002)
```
#### Actual Results
```
File "<stdin>", line 1, in <module>
  File ".../pydicom/pydicom/dataset.py", line 1284, in __str__
    return self._pretty_str()
  File ".../pydicom/pydicom/dataset.py", line 1022, in _pretty_str
    for data_element in self:
  File ".../pydicom/pydicom/dataset.py", line 751, in __iter__
    yield self[tag]
  File ".../pydicom/pydicom/dataset.py", line 637, in __getitem__
    self[tag] = DataElement_from_raw(data_elem, character_set)
  File ".../pydicom/pydicom/dataelem.py", line 447, in DataElement_from_raw
    raise NotImplementedError("{0:s} in tag {1!r}".format(str(e), raw.tag))
NotImplementedError: Unknown Value Representation '' in tag (0008, 0001)
```
[Or see here for another example](https://user-images.githubusercontent.com/28559755/51027486-4abf4100-1591-11e9-8f44-a739b00ca300.PNG)




Discussion:

Patch:
diff --git a/pydicom/values.py b/pydicom/values.py
--- a/pydicom/values.py
+++ b/pydicom/values.py
@@ -310,7 +310,12 @@ def convert_UR_string(byte_string, is_little_endian, struct_format=None):
 def convert_value(VR, raw_data_element, encodings=None):
     """Return the converted value (from raw bytes) for the given VR"""
     if VR not in converters:
-        message = "Unknown Value Representation '{0}'".format(VR)
+        # `VR` characters are in the ascii alphabet ranges 65 - 90, 97 - 122
+        char_range = list(range(65, 91)) + list(range(97, 123))
+        # If the VR characters are outside that range then print hex values
+        if ord(VR[0]) not in char_range or ord(VR[1]) not in char_range:
+            VR = ' '.join(['0x{:02x}'.format(ord(ch)) for ch in VR])
+        message = "Unknown Value Representation '{}'".format(VR)
         raise NotImplementedError(message)
 
     # Look up the function to convert that VR


Test patch:
diff --git a/pydicom/tests/test_filereader.py b/pydicom/tests/test_filereader.py
--- a/pydicom/tests/test_filereader.py
+++ b/pydicom/tests/test_filereader.py
@@ -15,7 +15,7 @@
 import pydicom.config
 from pydicom.dataset import Dataset, FileDataset
 from pydicom.data import get_testdata_files
-from pydicom.filereader import dcmread
+from pydicom.filereader import dcmread, read_dataset
 from pydicom.dataelem import DataElement, DataElement_from_raw
 from pydicom.errors import InvalidDicomError
 from pydicom.filebase import DicomBytesIO
@@ -689,6 +689,67 @@ def test_dcmread_does_not_raise(self):
             self.fail('Unexpected EOFError raised')
 
 
+class TestUnknownVR(object):
+    @pytest.mark.parametrize(
+        'vr_bytes, str_output',
+        [
+            # Test limits of char values
+            (b'\x00\x41', '0x00 0x41'),  # 000/A
+            (b'\x40\x41', '0x40 0x41'),  # 064/A
+            (b'\x5B\x41', '0x5b 0x41'),  # 091/A
+            (b'\x60\x41', '0x60 0x41'),  # 096/A
+            (b'\x7B\x41', '0x7b 0x41'),  # 123/A
+            (b'\xFF\x41', '0xff 0x41'),  # 255/A
+            # Test good/bad
+            (b'\x41\x00', '0x41 0x00'),  # A/-
+            (b'\x5A\x00', '0x5a 0x00'),  # Z/-
+            # Test not quite good/bad
+            (b'\x61\x00', '0x61 0x00'),  # a/-
+            (b'\x7A\x00', '0x7a 0x00'),  # z/-
+            # Test bad/good
+            (b'\x00\x41', '0x00 0x41'),  # -/A
+            (b'\x00\x5A', '0x00 0x5a'),  # -/Z
+            # Test bad/not quite good
+            (b'\x00\x61', '0x00 0x61'),  # -/a
+            (b'\x00\x7A', '0x00 0x7a'),  # -/z
+            # Test good/good
+            (b'\x41\x41', 'AA'),  # A/A
+            (b'\x41\x5A', 'AZ'),  # A/Z
+            (b'\x5A\x41', 'ZA'),  # Z/A
+            (b'\x5A\x5A', 'ZZ'),  # Z/Z
+            # Test not quite good
+            (b'\x41\x61', 'Aa'),  # A/a
+            (b'\x41\x7A', 'Az'),  # A/z
+            (b'\x61\x41', 'aA'),  # a/A
+            (b'\x61\x5A', 'aZ'),  # a/Z
+            (b'\x61\x61', 'aa'),  # a/a
+            (b'\x61\x7A', 'az'),  # a/z
+            (b'\x5A\x61', 'Za'),  # Z/a
+            (b'\x5A\x7A', 'Zz'),  # Z/z
+            (b'\x7A\x41', 'zA'),  # z/A
+            (b'\x7A\x5A', 'zZ'),  # z/Z
+            (b'\x7A\x61', 'za'),  # z/a
+            (b'\x7A\x7A', 'zz'),  # z/z
+        ]
+    )
+    def test_fail_decode_msg(self, vr_bytes, str_output):
+        """Regression test for #791."""
+        ds = read_dataset(
+            BytesIO(
+                b'\x08\x00\x01\x00' +
+                vr_bytes +
+                b'\x00\x00\x00\x08\x00\x49'
+            ),
+            False, True
+        )
+        msg = (
+            r"Unknown Value Representation '{}' in tag \(0008, 0001\)"
+            .format(str_output)
+        )
+        with pytest.raises(NotImplementedError, match=msg):
+            print(ds)
+
+
 class ReadDataElementTests(unittest.TestCase):
     def setUp(self):
         ds = Dataset()