Repository: cython/cython

Issue:
[BUG] Relative cimport from package without `__init__.pxd` fails
### Describe the bug

Relative cimport from package without `__init__.pxd` fails - see example below (adding `b/__init__.pxd` will fix the compilation issue):

```
$ cython -3 a.pyx

Error compiling Cython file:
------------------------------------------------------------
...
from .b cimport aa
^
------------------------------------------------------------

a.pyx:1:0: 't/b.pxd' not found

Error compiling Cython file:
------------------------------------------------------------
...
from .b cimport aa

cdef aa.ulong a = 5
     ^
------------------------------------------------------------

a.pyx:3:5: 'aa' is not declared
```

### Code to reproduce the behaviour:

```
######## __init__.py ########

######## a.pyx ########
from .b cimport aa
cdef aa.ulong a = 5

######## b/__init__.py ########

######## b/aa.pxd ########
ctypedef long int ulong
```

### Expected behaviour

The example is compiled successfuly even without `b/__init__.pxd`

### OS

any

### Python version

any

### Cython version

3.0.1, 3.0.2, master

### Additional context

Affecting: https://github.com/MagicStack/uvloop/pull/534
Possible introduced by: #5538


Discussion:
Oh, yes, that seems a serious regression. Targeting for 3.0.3 to make sure we keep it in sight.
Yes it is caused by following lines introduced in #5538:
https://github.com/cython/cython/blob/9827c6085e2141db71c55ae231a4a09a878dd524/Cython/Compiler/Main.py#L222-L224

Feel free to revert #5538 if needed.

Resolving patch:
diff --git a/Cython/Compiler/Main.py b/Cython/Compiler/Main.py
--- a/Cython/Compiler/Main.py
+++ b/Cython/Compiler/Main.py
@@ -145,7 +145,7 @@ def process_pxd(self, source_desc, scope, module_name):
     def nonfatal_error(self, exc):
         return Errors.report_error(exc)
 
-    def _split_qualified_name(self, qualified_name):
+    def _split_qualified_name(self, qualified_name, relative_import=False):
         # Splits qualified_name into parts in form of 2-tuples: (PART_NAME, IS_PACKAGE).
         qualified_name_parts = qualified_name.split('.')
         last_part = qualified_name_parts.pop()
@@ -156,7 +156,7 @@ def _split_qualified_name(self, qualified_name):
             is_package = False
             for suffix in ('.py', '.pyx'):
                 path = self.search_include_directories(
-                    qualified_name, suffix=suffix, source_pos=None, source_file_path=None)
+                    qualified_name, suffix=suffix, source_pos=None, source_file_path=None, sys_path=not relative_import)
                 if path:
                     is_package = self._is_init_file(path)
                     break
@@ -219,16 +219,13 @@ def find_module(self, module_name, from_module=None, pos=None, need_pxd=1,
                 if pxd_pathname:
                     is_package = self._is_init_file(pxd_pathname)
                     scope = from_module.find_submodule(module_name, as_package=is_package)
-        if not scope and relative_import:
-            error(pos, "'%s.pxd' not found" % qualified_name.replace('.', os.sep))
-            return None
         if not scope:
             if debug_find_module:
                 print("...trying absolute import")
             if absolute_fallback:
                 qualified_name = module_name
             scope = self
-            for name, is_package in self._split_qualified_name(qualified_name):
+            for name, is_package in self._split_qualified_name(qualified_name, relative_import=relative_import):
                 scope = scope.find_submodule(name, as_package=is_package)
         if debug_find_module:
             print("...scope = %s" % scope)
@@ -240,7 +237,7 @@ def find_module(self, module_name, from_module=None, pos=None, need_pxd=1,
                     print("...looking for pxd file")
                 # Only look in sys.path if we are explicitly looking
                 # for a .pxd file.
-                pxd_pathname = self.find_pxd_file(qualified_name, pos, sys_path=need_pxd)
+                pxd_pathname = self.find_pxd_file(qualified_name, pos, sys_path=need_pxd and not relative_import)
                 self._check_pxd_filename(pos, pxd_pathname, qualified_name)
                 if debug_find_module:
                     print("......found %s" % pxd_pathname)
@@ -249,7 +246,7 @@ def find_module(self, module_name, from_module=None, pos=None, need_pxd=1,
                     # look for the non-existing pxd file next time.
                     scope.pxd_file_loaded = True
                     package_pathname = self.search_include_directories(
-                        qualified_name, suffix=".py", source_pos=pos)
+                        qualified_name, suffix=".py", source_pos=pos, sys_path=not relative_import)
                     if package_pathname and package_pathname.endswith(Utils.PACKAGE_FILES):
                         pass
                     else:
@@ -284,14 +281,14 @@ def find_pxd_file(self, qualified_name, pos=None, sys_path=True, source_file_pat
         pxd = self.search_include_directories(
             qualified_name, suffix=".pxd", source_pos=pos, sys_path=sys_path, source_file_path=source_file_path)
         if pxd is None and Options.cimport_from_pyx:
-            return self.find_pyx_file(qualified_name, pos)
+            return self.find_pyx_file(qualified_name, pos, sys_path=sys_path)
         return pxd
 
-    def find_pyx_file(self, qualified_name, pos=None, source_file_path=None):
+    def find_pyx_file(self, qualified_name, pos=None, sys_path=True, source_file_path=None):
         # Search include path for the .pyx file corresponding to the
         # given fully-qualified module name, as for find_pxd_file().
         return self.search_include_directories(
-            qualified_name, suffix=".pyx", source_pos=pos, source_file_path=source_file_path)
+            qualified_name, suffix=".pyx", source_pos=pos, sys_path=sys_path, source_file_path=source_file_path)
 
     def find_include_file(self, filename, pos=None, source_file_path=None):
         # Search list of include directories for filename.


Test patch:
diff --git a/tests/run/relative_cimport.srctree b/tests/run/relative_cimport.srctree
--- a/tests/run/relative_cimport.srctree
+++ b/tests/run/relative_cimport.srctree
@@ -2,12 +2,38 @@
 # tag: cimport
 
 PYTHON setup.py build_ext --inplace
+PYTHON -c "from py_pkg.py_a import test; test()"
 PYTHON -c "from pkg.a import test; assert test() == (5, 7)"
 PYTHON -c "from pkg.b import test; assert test() == (1, 2)"
 PYTHON -c "from pkg.b_py2 import test; assert test() == (1, 2)"
 PYTHON -c "from pkg.sub.c import test; assert test() == (1, 2)"
 PYTHON -c "from pkg.sub.d import test; assert test() == 3"
 
+# File structure:
+# ├── setup.py
+# ├── py_pkg/
+# |   ├── __init__.py
+# |   ├── py_a.pyx
+# |   ├── py_b.pxd
+# |   └── py_sub/
+# |       ├── __init__.py
+# |       ├── py_c.pxd
+# |       └── py_d.pxd
+# └── pkg/
+#     ├── __init__.py
+#     ├── a.pyx
+#     ├── a.pxd
+#     ├── b.pyx
+#     ├── b_py2.pyx
+#     └── sub/
+#         ├── __init__.py
+#         ├── __init__.pxd
+#         ├── c.pyx
+#         ├── c.pxd
+#         ├── d.pyx
+#         ├── tdef.pxd
+#         └── reimport.pxd
+
 ######## setup.py ########
 
 from distutils.core import setup
@@ -19,6 +45,37 @@ setup(
 )
 
 
+######## py_pkg/__init__.py ########
+
+######## py_pkg/py_a.pyx ########
+
+from .py_sub cimport py_c
+
+def test():
+    cdef py_c.py_b.py_b_int b = 1
+    assert b == 1
+    cdef py_c.py_c_int c = 2
+    assert c == 2
+    cdef py_c.py_d.py_d_int d = 3
+    assert d == 3
+
+######## py_pkg/py_b.pxd ########
+
+ctypedef int py_b_int
+
+######## py_pkg/py_sub/__init__.py ########
+
+######## py_pkg/py_sub/py_c.pxd ########
+
+from .. cimport py_b
+from . cimport py_d
+
+ctypedef int py_c_int
+
+######## py_pkg/py_sub/py_d.pxd ########
+
+ctypedef int py_d_int
+
 ######## pkg/__init__.py ########
 
 ######## pkg/sub/__init__.py ########