Repository: falconry/falcon

Issue:
Tox (including Travis) not testing cythonized variants
As described in ``README.rst``, Falcon can be cythonized for ~20% performance gain (or actually even more). Installing Falcon from *sdist* into an environment with Cython does the trick:

```python
>>> import falcon
>>> falcon.api  # As we can see, falcon.api is coming from the dynamically-linked (cythonized) library api.so
<module 'falcon.api' from '/home/vytas/.virtualenvs/fresh/local/lib/python2.7/site-packages/falcon/api.so'>
```

However, this does not hold under Tox ``py27_cython`` and ``py36_cython`` environments, including runs in Travis, as the properly cythonized Falcon is shadowed by the local source directory. This could potentially be worked around by changing dir in Tox, but apparently pytest is even more stubborn as it is correctly determining the root dir of tests, and changing to it.

See also discussions here:
* https://github.com/tox-dev/tox/issues/54
* https://github.com/tox-dev/tox/issues/514

The last comment on the latter also explains the possible patterns to work this around: https://github.com/tox-dev/tox/issues/514#issuecomment-327779367 (links to the useful https://docs.pytest.org/en/latest/goodpractices.html#choosing-a-test-layout-import-rules ).



Discussion:

Resolving patch:
diff --git a/falcon/cmd/print_routes.py b/falcon/cmd/print_routes.py
--- a/falcon/cmd/print_routes.py
+++ b/falcon/cmd/print_routes.py
@@ -58,13 +58,19 @@ def traverse(roots, parent='', verbose=False):
                         else:
                             real_func = func
 
-                        source_file = inspect.getsourcefile(real_func)
-
-                        print('-->{0} {1}:{2}'.format(
-                            method,
-                            source_file,
-                            source_file[1]
-                        ))
+                        try:
+                            source_file = inspect.getsourcefile(real_func)
+                            source_lines = inspect.getsourcelines(real_func)
+                            source_info = '{}:{}'.format(source_file,
+                                                         source_lines[1])
+                        except TypeError:
+                            # NOTE(vytas): If Falcon is cythonized, all default
+                            # responders coming from cythonized modules will
+                            # appear as built-in functions, and raise a
+                            # TypeError when trying to locate the source file.
+                            source_info = '[unknown file]'
+
+                        print('-->' + method, source_info)
 
         if root.children:
             traverse(root.children, parent + '/' + root.raw_segment, verbose)
@@ -92,7 +98,7 @@ def main():
     except ValueError:
         parser.error(
             'The api_module must include a colon between '
-            'the module and instnace')
+            'the module and instance')
     api = getattr(__import__(module, fromlist=[True]), instance)
     if not isinstance(api, falcon.API):
         if callable(api):


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
diff --git a/tests/test_cmd_print_api.py b/tests/test_cmd_print_api.py
--- a/tests/test_cmd_print_api.py
+++ b/tests/test_cmd_print_api.py
@@ -3,6 +3,11 @@
 from falcon.testing import redirected
 from falcon.util import compat
 
+try:
+    import cython
+except ImportError:
+    cython = None
+
 
 class DummyResource(object):
 
@@ -31,10 +36,16 @@ def test_traverse_with_verbose():
         get_info, options_info = options_info, get_info
 
     assert options_info.startswith('-->OPTIONS')
-    assert 'falcon/responders.py:' in options_info
+    if cython:
+        assert options_info.endswith('[unknown file]')
+    else:
+        assert 'falcon/responders.py:' in options_info
 
     assert get_info.startswith('-->GET')
-    assert 'tests/test_cmd_print_api.py:' in get_info
+    # NOTE(vytas): This builds upon the fact that on_get is defined on line 14
+    # in this file. Adjust the test if the said responder is relocated, or just
+    # check for any number if this becomes too painful to maintain.
+    assert get_info.endswith('tests/test_cmd_print_api.py:14')
 
 
 def test_traverse():
diff --git a/tests/test_cython.py b/tests/test_cython.py
new file mode 100644
--- /dev/null
+++ b/tests/test_cython.py
@@ -0,0 +1,15 @@
+import pytest
+
+import falcon
+
+try:
+    import cython
+except ImportError:
+    cython = None
+
+
+class TestCythonized(object):
+
+    @pytest.mark.skipif(not cython, reason='Cython not installed')
+    def test_imported_from_c_modules(self):
+        assert 'falcon/api.py' not in str(falcon.api)