Repository: cython/cython

Issue:
BUG: cannot easily override a cython Include
I am trying to vendor `numpy.pxd` (or `numpy/__init__.pxd`) into numpy. It seems the file in `Includes/numpy/__init__.pxd` is seen before mine when I try `cimport numpy`.

To reproduce: copy the `__init__.pxd` from `Includes/numpy` into `site-packages/numpy`, and add garbage in the original file so using it will crash cython (I used `xxxx = xxxxxxxxx`). Then build a project that calls `cimport numpy` such as pandas. If the copy is found, `cythonize` will succeed. If the original version is found, cythonize will crash.

I am willing to work on a PR, but need some direction as to where to look for the problem.


Discussion:
I think the problem is that `Includes` is appended to the search path [here](https://github.com/cython/cython/blob/0.29.6/Cython/Compiler/Main.py#L93), then later on `sys.path` is optionally appended to the search path [here](https://github.com/cython/cython/blob/0.29.6/Cython/Utils.py#L136). IMO if `sys_path` is True the precedence should be 
- user provided `include_directories`
- `sys.path`
- `Includes`

instead of the current situation which gives precedence to `Includes`
I agree about the import path order. `sys.path` and `Includes/` should be reversed.
PR welcome.

Resolving patch:
diff --git a/Cython/Compiler/Main.py b/Cython/Compiler/Main.py
--- a/Cython/Compiler/Main.py
+++ b/Cython/Compiler/Main.py
@@ -40,6 +40,8 @@
 
 verbose = 0
 
+standard_include_path = os.path.abspath(os.path.join(os.path.dirname(__file__),
+                                        os.path.pardir, 'Includes'))
 
 class Context(object):
     #  This class encapsulates the context needed for compiling
@@ -75,10 +77,6 @@ def __init__(self, include_directories, compiler_directives, cpp=False,
         self.pxds = {}  # full name -> node tree
         self._interned = {}  # (type(value), value, *key_args) -> interned_value
 
-        standard_include_path = os.path.abspath(os.path.normpath(
-            os.path.join(os.path.dirname(__file__), os.path.pardir, 'Includes')))
-        self.include_directories = include_directories + [standard_include_path]
-
         if language_level is not None:
             self.set_language_level(language_level)
 
@@ -253,8 +251,13 @@ def find_include_file(self, filename, pos):
 
     def search_include_directories(self, qualified_name, suffix, pos,
                                    include=False, sys_path=False):
-        return search_include_directories(
-            tuple(self.include_directories), qualified_name, suffix, pos, include, sys_path)
+        include_dirs = self.include_directories
+        if sys_path:
+            include_dirs = include_dirs + sys.path
+        # include_dirs must be hashable for caching in @cached_function
+        include_dirs = tuple(include_dirs + [standard_include_path])
+        return search_include_directories(include_dirs, qualified_name,
+                                          suffix, pos, include)
 
     def find_root_package_dir(self, file_path):
         return Utils.find_root_package_dir(file_path)
@@ -602,8 +605,7 @@ def compile(source, options = None, full_module_name = None, **kwds):
 
 
 @Utils.cached_function
-def search_include_directories(dirs, qualified_name, suffix, pos,
-                               include=False, sys_path=False):
+def search_include_directories(dirs, qualified_name, suffix, pos, include=False):
     """
     Search the list of include directories for the given file name.
 
@@ -612,10 +614,7 @@ def search_include_directories(dirs, qualified_name, suffix, pos,
     report an error.
 
     The 'include' option will disable package dereferencing.
-    If 'sys_path' is True, also search sys.path.
     """
-    if sys_path:
-        dirs = dirs + tuple(sys.path)
 
     if pos:
         file_desc = pos[0]
@@ -648,7 +647,7 @@ def search_include_directories(dirs, qualified_name, suffix, pos,
                 path = os.path.join(package_dir, module_filename)
                 if os.path.exists(path):
                     return path
-                path = os.path.join(dirname, package_dir, module_name,
+                path = os.path.join(package_dir, module_name,
                                     package_filename)
                 if os.path.exists(path):
                     return path


Test patch:
diff --git a/tests/compile/find_pxd.srctree b/tests/compile/find_pxd.srctree
--- a/tests/compile/find_pxd.srctree
+++ b/tests/compile/find_pxd.srctree
@@ -6,12 +6,13 @@ from Cython.Build import cythonize
 from Cython.Distutils.extension import Extension
 
 import sys
-sys.path.append("path")
+sys.path.insert(0, "path")
 
 ext_modules = [
     Extension("a", ["a.pyx"]),
     Extension("b", ["b.pyx"]),
     Extension("c", ["c.pyx"]),
+    Extension("d", ["d.pyx"]),
 ]
 
 ext_modules = cythonize(ext_modules, include_path=["include"])
@@ -37,3 +38,15 @@ ctypedef int my_type
 
 ######## path/c.pxd ########
 +++syntax error just to show that this file is not actually cimported+++
+
+######## path/numpy/__init__.pxd ########
+
+# gh-2905: This should be found before Cython/Inlude/numpy/__init__.pxd
+
+ctypedef int my_type
+
+######## d.pyx ########
+
+cimport numpy
+
+cdef numpy.my_type foo