Repository: numpy/numpy

Issue:
Make ctypes completely optional on Windows
There are a couple of places where `ctypes` is presumed to be available, which means `numpy` cannot be imported on systems/distributions that do not include it.

These are [`_dtype_ctypes.py`](https://github.com/numpy/numpy/blob/master/numpy/core/_dtype_ctypes.py), and the [`_distributor_init.py`](https://github.com/numpy/numpy/blob/bfb156741a92f77c13c50d82fc5a1f4d6177a9f5/tools/openblas_support.py#L197) generated for `libopenblas`.

The `_dtype_ctypes` module seems easy enough to update to match the other ctypes-using modules (or simply delay the [eager import](https://github.com/numpy/numpy/blob/master/numpy/core/__init__.py#L100) or handle it failing). I can't find any uses in numpy itself though, so I have no idea what its compatibility constraints are.

The `_distributor_init.py` file is somewhat redundant with the [generated `__config__.py`](https://github.com/numpy/numpy/blob/8087e1de04d011f7ce66bf80dbef4a0599b14f7a/numpy/distutils/misc_util.py#L2289), but could also just do nothing when ctypes fails to import.


I'm happy to send a PR, but it's not obvious which fixes would be preferred. If I were going to keep it simple, I'd defer the `import ctypes` in `_dtype_ctypes` into the function(s) that use it, and add an early exit from the `_distributor_init.py` when ctypes is unavailable.


Discussion:
> I can't find any uses in numpy itself though, so I have no idea what its compatibility constraints are.

It's imported by the c extension module, to do things with ctypes which are far too much work from the C API.
The use in distributor_init is windows only, right? Is that ever missing ctypes anyway?
> The use in distributor_init is windows only, right? Is that ever missing ctypes anyway?

It is if you care enough about security to disable it :)

> It's imported by the c extension module

Okay, thanks for the pointer. But it looks like it is only used when you have passed in ctypes values? Which means if you're not using ctypes, you'll never hit those code paths?
> Which means if you're not using ctypes, you'll never hit those code paths?

Correct. It's possible the module isn't even imported in that case.
@zooba just cruious, but I am a bit wondering about the security part. If you disable ctypes for security, it seems to me that allowing full NumPy API access would be an issue. Of it is much less convenient convenient then using ctypes directly, but probably not much more than a small layer of obfuscation? (The main issue is the `__array_interface__`, since it allows passing data by pointer.)
> Correct. It's possible the module isn't even imported in that case.

Unfortunately not, it's imported as part of `numpy/core/__init__.py` --> https://github.com/numpy/numpy/blob/master/numpy/core/__init__.py#L100

> If you disable ctypes for security, it seems to me that allowing full NumPy API access would be an issue.

Yeah, you're probably right. I'll deal with that one when our pentesters discover it ;) Thanks for the heads-up though.

Resolving patch:
diff --git a/numpy/core/_dtype_ctypes.py b/numpy/core/_dtype_ctypes.py
--- a/numpy/core/_dtype_ctypes.py
+++ b/numpy/core/_dtype_ctypes.py
@@ -22,9 +22,10 @@ class DummyStruct(ctypes.Structure):
 * PEP3118 cannot represent unions, but both numpy and ctypes can
 * ctypes cannot handle big-endian structs with PEP3118 (bpo-32780)
 """
-import _ctypes
-import ctypes
 
+# We delay-import ctypes for distributions that do not include it.
+# While this module is not used unless the user passes in ctypes
+# members, it is eagerly imported from numpy/core/__init__.py.
 import numpy as np
 
 
@@ -39,6 +40,7 @@ def _from_ctypes_structure(t):
                 "ctypes bitfields have no dtype equivalent")
 
     if hasattr(t, "_pack_"):
+        import ctypes
         formats = []
         offsets = []
         names = []
@@ -79,6 +81,7 @@ def _from_ctypes_scalar(t):
 
 
 def _from_ctypes_union(t):
+    import ctypes
     formats = []
     offsets = []
     names = []
@@ -98,6 +101,7 @@ def dtype_from_ctypes_type(t):
     """
     Construct a dtype object from a ctypes type
     """
+    import _ctypes
     if issubclass(t, _ctypes.Array):
         return _from_ctypes_array(t)
     elif issubclass(t, _ctypes._Pointer):
diff --git a/tools/openblas_support.py b/tools/openblas_support.py
--- a/tools/openblas_support.py
+++ b/tools/openblas_support.py
@@ -207,12 +207,12 @@ def make_init(dirname):
             and is created as part of the scripts that build the wheel.
             '''
             import os
-            from ctypes import WinDLL
             import glob
             if os.name == 'nt':
                 # convention for storing / loading the DLL from
                 # numpy/.libs/, if present
                 try:
+                    from ctypes import WinDLL
                     basedir = os.path.dirname(__file__)
                 except:
                     pass
@@ -221,16 +221,16 @@ def make_init(dirname):
                     DLL_filenames = []
                     if os.path.isdir(libs_dir):
                         for filename in glob.glob(os.path.join(libs_dir,
-                                                             '*openblas*dll')):
+                                                               '*openblas*dll')):
                             # NOTE: would it change behavior to load ALL
                             # DLLs at this path vs. the name restriction?
                             WinDLL(os.path.abspath(filename))
                             DLL_filenames.append(filename)
-                if len(DLL_filenames) > 1:
-                    import warnings
-                    warnings.warn("loaded more than 1 DLL from .libs:\\n%s" %
-                              "\\n".join(DLL_filenames),
-                              stacklevel=1)
+                    if len(DLL_filenames) > 1:
+                        import warnings
+                        warnings.warn("loaded more than 1 DLL from .libs:\\n%s" %
+                                      "\\n".join(DLL_filenames),
+                                      stacklevel=1)
     """))
 
 def test_setup(arches):


Test patch: