Repository: pydicom/pydicom

Issue:
Mypy errors
**Describe the bug**
Several of the type hints are problematic and result in mypy errors.

One example:

```none
cat << EOF > /tmp/test.py
from pydicom import Dataset, dcmread

dataset = Dataset()
dataset.Rows = 10
dataset.Columns = 20
dataset.NumberOfFrames = "5"

assert int(dataset.NumberOfFrames) == 5

filename = '/tmp/test.dcm'
dataset.save_as(str(filename))

dataset = dcmread(filename)

assert int(dataset.NumberOfFrames) == 5
EOF
```

```none
mypy /tmp/test.py
/tmp/test.py:15: error: No overload variant of "int" matches argument type "object"
/tmp/test.py:15: note: Possible overload variant:
/tmp/test.py:15: note:     def int(self, x: Union[str, bytes, SupportsInt, _SupportsIndex] = ...) -> int
/tmp/test.py:15: note:     <1 more non-matching overload not shown>
Found 1 error in 1 file (checked 1 source file)
```

**Expected behavior**
Mypy should not report any errors.

**Steps To Reproduce**
See above

**Your environment**
```none
python -m pydicom.env_info
module       | version
------       | -------
platform     | macOS-10.15.6-x86_64-i386-64bit
Python       | 3.8.6 (default, Oct  8 2020, 14:06:32)  [Clang 12.0.0 (clang-1200.0.32.2)]
pydicom      | 2.1.0
gdcm         | _module not found_
jpeg_ls      | _module not found_
numpy        | 1.19.3
PIL          | 8.0.1
```
ImportError: cannot import name 'NoReturn'
**Describe the bug**
throw following excetion when import pydicom package:
```
xxx/python3.6/site-packages/pydicom/filebase.py in <module>
5 from struct import unpack, pack
      6 from types import TracebackType
----> 7 from typing import (
      8     Tuple, Optional, NoReturn, BinaryIO, Callable, Type, Union, cast, TextIO,
      9     TYPE_CHECKING, Any

ImportError: cannot import name 'NoReturn'
```

**Expected behavior**
imort pydicom sucessfully

**Steps To Reproduce**
How to reproduce the issue. Please include a minimum working code sample, the
traceback (if any) and the anonymized DICOM dataset (if relevant).

**Your environment**
python:3.6.0
pydicom:2.1



Discussion:
I ran into multiple other errors and would suggest removing `py.typed` until the type annotations have been properly tested. 
@hackermd, just for my education, since I have only dabbled in 'typing'  so far - does this break your workflow somehow?  Is it not possible to exclude pydicom from forcing errors? (other than removing `py.typed`)

Hmm, maybe we should just use `Any` for the element values rather than `object`. The problem is the element value type will be/is so broad users are pretty much going to have to `cast` everything no matter what we do.
@darcymason 

> does this break your workflow somehow

We are running mypy on all our Pyhon code and our unit test pipelines are failing. I have fixed the pydicom version to `2.0.0`, but I would like to avoid doing that moving forward.

>  Is it not possible to exclude pydicom from forcing errors? (other than removing py.typed)

One can add `#type: ingore` to exclude individual lines from type checks, but at the moment we are getting hundreds of errors.

Instead of removing `py.typed`, one could also remove the type hints from problematic functions. `object` or `Any` are not that useful and seem to cause more trouble than benefit.
@scaramallion 

> The problem is the element value type will be/is so broad users are pretty much going to have to cast everything no matter what we do

Agreed. The return value of these "magic" methods will be difficult to type. It's basically a Union of all Python types corresponding to any of the DICOM Value Representations.
@hackermd, thanks for the explanations.

I suggest doing a patch release for this if @scaramallion is in agreement.  We could just remove the `py.typed`, as suggested, and do it quickly (and push 'typing' to v2.2),  or perhaps within a few days/couple of weeks if removal of `object` can be a better solution. @scaramallion, your thoughts?  

@hackermd, would you be able to test on master if we try the correction route?

> would you be able to test on master if we try the correction route

Yes, happy to help with that. 
Yeah, lets do it. Fix #1253 and #1254 while we're at it. Pushing typing back to at least v2.2 is probably the way to go, too. 
New in 3.6.2, well that's annoying

Patch:
diff --git a/pydicom/_version.py b/pydicom/_version.py
--- a/pydicom/_version.py
+++ b/pydicom/_version.py
@@ -3,7 +3,7 @@
 from typing import Tuple
 
 
-__version__: str = '2.1.0'
+__version__: str = '2.1.1'
 __version_info__: Tuple[str, str, str] = tuple(
     re.match(r'(\d+\.\d+\.\d+).*', __version__).group(1).split('.')
 )
diff --git a/pydicom/filebase.py b/pydicom/filebase.py
--- a/pydicom/filebase.py
+++ b/pydicom/filebase.py
@@ -5,7 +5,7 @@
 from struct import unpack, pack
 from types import TracebackType
 from typing import (
-    Tuple, Optional, NoReturn, BinaryIO, Callable, Type, Union, cast, TextIO,
+    Tuple, Optional, BinaryIO, Callable, Type, Union, cast, TextIO,
     TYPE_CHECKING, Any
 )
 
@@ -182,15 +182,15 @@ def __init__(
         self.close = file_like_obj.close
         self.name: str = getattr(file_like_obj, 'name', '<no filename>')
 
-    def no_write(self, bytes_read: bytes) -> NoReturn:
+    def no_write(self, bytes_read: bytes) -> None:
         """Used for file-like objects where no write is available"""
         raise IOError("This DicomFileLike object has no write() method")
 
-    def no_read(self, bytes_read: Optional[int] = None) -> NoReturn:
+    def no_read(self, bytes_read: Optional[int] = None) -> None:
         """Used for file-like objects where no read is available"""
         raise IOError("This DicomFileLike object has no read() method")
 
-    def no_seek(self, offset: int, from_what: int = 0) -> NoReturn:
+    def no_seek(self, offset: int, from_what: int = 0) -> None:
         """Used for file-like objects where no seek is available"""
         raise IOError("This DicomFileLike object has no seek() method")
 
diff --git a/pydicom/jsonrep.py b/pydicom/jsonrep.py
--- a/pydicom/jsonrep.py
+++ b/pydicom/jsonrep.py
@@ -226,7 +226,8 @@ def get_sequence_item(self, value):
                     value_key = unique_value_keys[0]
                     elem = DataElement.from_json(
                         self.dataset_class, key, vr,
-                        val[value_key], value_key
+                        val[value_key], value_key,
+                        self.bulk_data_element_handler
                     )
                 ds.add(elem)
         return ds
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -38,6 +38,7 @@
     "Programming Language :: Python :: 3.6",
     "Programming Language :: Python :: 3.7",
     "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
     "Operating System :: OS Independent",
     "Topic :: Scientific/Engineering :: Medical Science Apps.",
     "Topic :: Scientific/Engineering :: Physics",
@@ -80,7 +81,7 @@ def data_files_inventory():
 
 
 PACKAGE_DATA = {
-    'pydicom': data_files_inventory() + ['py.typed']
+    'pydicom': data_files_inventory()
 }
 
 opts = dict(


Test patch:
diff --git a/pydicom/tests/test_json.py b/pydicom/tests/test_json.py
--- a/pydicom/tests/test_json.py
+++ b/pydicom/tests/test_json.py
@@ -354,3 +354,25 @@ def bulk_data_reader(tag, vr, value):
         ds = Dataset().from_json(json.dumps(json_data), bulk_data_reader)
 
         assert b'xyzzy' == ds[0x00091002].value
+
+    def test_bulk_data_reader_is_called_within_SQ(self):
+        def bulk_data_reader(_):
+            return b'xyzzy'
+
+        json_data = {
+            "003a0200": {
+                "vr": "SQ",
+                "Value": [
+                    {
+                        "54001010": {
+                            "vr": "OW",
+                            "BulkDataURI": "https://a.dummy.url"
+                        }
+                    }
+                ]
+            }
+        }
+
+        ds = Dataset().from_json(json.dumps(json_data), bulk_data_reader)
+
+        assert b'xyzzy' == ds[0x003a0200].value[0][0x54001010].value