From aadb8ed913892fa8d4b7d49b9fb77ae1f257bfd4 Mon Sep 17 00:00:00 2001
From: mattip <matti.picus@gmail.com>
Date: Sun, 31 Mar 2019 19:16:48 +0300
Subject: [PATCH] BUG: prefer sys.path before Cython/Include when searching for
 pxd files

---
 Cython/Compiler/Main.py        | 38 ++++++++++++++++++++++------------
 tests/compile/find_pxd.srctree | 15 +++++++++++++-
 2 files changed, 39 insertions(+), 14 deletions(-)

diff --git a/Cython/Compiler/Main.py b/Cython/Compiler/Main.py
index da4c3cc07..bf648a605 100644
--- a/Cython/Compiler/Main.py
+++ b/Cython/Compiler/Main.py
@@ -40,6 +40,8 @@ module_name_pattern = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_
 
 verbose = 0
 
+standard_include_path = os.path.abspath(os.path.normpath(
+        os.path.join(os.path.dirname(__file__), os.path.pardir, 'Includes')))
 
 class Context(object):
     #  This class encapsulates the context needed for compiling
@@ -66,7 +68,7 @@ class Context(object):
         self.modules = {"__builtin__" : Builtin.builtin_scope}
         self.cython_scope = CythonScope.create_cython_scope(self)
         self.modules["cython"] = self.cython_scope
-        self.include_directories = include_directories
+        self.include_directories = tuple(include_directories)
         self.future_directives = set()
         self.compiler_directives = compiler_directives
         self.cpp = cpp
@@ -75,10 +77,6 @@ class Context(object):
         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 @@ class Context(object):
 
     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)
+        if sys_path:
+            include_dirs = self.include_directories + tuple(sys.path)
+        else:
+            include_dirs = self.include_directories
+        include_dirs = 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)
@@ -274,8 +277,10 @@ class Context(object):
             return 1
         for kind, name in self.read_dependency_file(source_path):
             if kind == "cimport":
+                # missing suffix?
                 dep_path = self.find_pxd_file(name, pos)
             elif kind == "include":
+                # missing suffix?
                 dep_path = self.search_include_directories(name, pos)
             else:
                 continue
@@ -602,8 +607,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 +616,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 +649,18 @@ 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,
+                # In most cases, dirname and package_dir will be the same.
+                # From the documentation of os.path.join:
+                # " If a component is an absolute path, all previous components
+                # are thrown away and joining continues from the absolute path
+                # component"
+                # So if dirname and package_dir are absolute pathes, one will
+                # be discarded. However what happens when they are relative
+                # single-component paths? They will be concatenated (repeated),
+                # causing rare and hard to debug problems.
+                # path = os.path.join(dirname, package_dir, module_name,
+                #                    package_filename)
+                path = os.path.join(package_dir, module_name,
                                     package_filename)
                 if os.path.exists(path):
                     return path
diff --git a/tests/compile/find_pxd.srctree b/tests/compile/find_pxd.srctree
index 1d7885fe8..75d2765c9 100644
--- 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
-- 
2.30.9