Repository: numpy/numpy
Pull request: BUG: use tmp dir and check version for cython test
Fixes gh-15166

- Check the cython version
- Use a subprocess to run the test
- Run the test in a [temporary directory](https://docs.pytest.org/en/latest/tmpdir.html#the-default-base-temporary-directory).
- Clean up a warning

Problem statement:
BUG, TST: random/test/test_extending:test_cython tries to write in wrong directory
<!-- Please describe the issue in detail here, and fill in the fields below -->
One of the tests fails, the previous version of numpy + earlier release of icc worked fine.
### Reproducing code example:

<!-- A short code example that reproduces the problem/missing feature. It should be
self-contained, i.e., possible to run as-is via 'python myproblem.py' -->

```python
python -c 'import numpy; numpy.test("full");'
```


```
_________________________________________________________________________________________ TestKind.test_all __________________________________________________________________________________________

self = <numpy.f2py.tests.test_kind.TestKind object at 0x7f8856d5c9d0>

    @pytest.mark.slow
    def test_all(self):
        selectedrealkind = self.module.selectedrealkind
        selectedintkind = self.module.selectedintkind
    
        for i in range(40):
            assert_(selectedintkind(i) in [selected_int_kind(i), -1],
                    'selectedintkind(%s): expected %r but got %r' %
                    (i, selected_int_kind(i), selectedintkind(i)))
    
        for i in range(20):
>           assert_(selectedrealkind(i) in [selected_real_kind(i), -1],
                    'selectedrealkind(%s): expected %r but got %r' %
                    (i, selected_real_kind(i), selectedrealkind(i)))
E           AssertionError: selectedrealkind(16): expected 10 but got 16

i          = 16
selectedintkind = <fortran object>
selectedrealkind = <fortran object>
self       = <numpy.f2py.tests.test_kind.TestKind object at 0x7f8856d5c9d0>

/usr/lib/python3.8/site-packages/numpy/f2py/tests/test_kind.py:32: AssertionError
____________________________________________________________________________________________ test_cython _____________________________________________________________________________________________

    @pytest.mark.skipif(cython is None, reason="requires cython")
    def test_cython():
        curdir = os.getcwd()
        argv = sys.argv
        examples = (os.path.dirname(__file__), '..', '_examples')
        try:
            os.chdir(os.path.join(*examples))
            sys.argv = argv[:1] + ['build']
            with warnings.catch_warnings(record=True) as w:
                # setuptools issue gh-1885
                warnings.filterwarnings('always', '', DeprecationWarning)
>               from numpy.random._examples.cython import setup

argv       = ['-c']
curdir     = '/home/zerg/Work/python-numpy-mkl'
examples   = ('/usr/lib/python3.8/site-packages/numpy/random/tests', '..', '_examples')
w          = [<warnings.WarningMessage object at 0x7f884e7361f0>]

/usr/lib/python3.8/site-packages/numpy/random/tests/test_extending.py:39: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/lib/python3.8/site-packages/numpy/random/_examples/cython/setup.py:30: in <module>
    ext_modules=cythonize(extensions)
/usr/lib/python3.8/site-packages/Cython/Build/Dependencies.py:1096: in cythonize
    cythonize_one(*args)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

pyx_file = '/usr/lib/python3.8/site-packages/numpy/random/_examples/cython/extending.pyx', c_file = '/usr/lib/python3.8/site-packages/numpy/random/_examples/cython/extending.c', fingerprint = None
quiet = False, options = <Cython.Compiler.Main.CompilationOptions object at 0x7f88540d5fa0>, raise_on_failure = True
embedded_metadata = {'distutils': {'depends': ['/usr/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h', '/usr/lib/python...ources': ['/usr/lib/python3.8/site-packages/numpy/random/_examples/cython/extending.pyx']}, 'module_name': 'extending'}
full_module_name = 'extending', progress = '[1/2] '

    @record_results
    def cythonize_one(pyx_file, c_file, fingerprint, quiet, options=None,
                      raise_on_failure=True, embedded_metadata=None, full_module_name=None,
                      progress=""):
        from ..Compiler.Main import compile_single, default_options
        from ..Compiler.Errors import CompileError, PyrexError
    
        if fingerprint:
            if not os.path.exists(options.cache):
                safe_makedirs(options.cache)
            # Cython-generated c files are highly compressible.
            # (E.g. a compression ratio of about 10 for Sage).
            fingerprint_file_base = join_path(
                options.cache, "%s-%s" % (os.path.basename(c_file), fingerprint))
            gz_fingerprint_file = fingerprint_file_base + gzip_ext
            zip_fingerprint_file = fingerprint_file_base + '.zip'
            if os.path.exists(gz_fingerprint_file) or os.path.exists(zip_fingerprint_file):
                if not quiet:
                    print("%sFound compiled %s in cache" % (progress, pyx_file))
                if os.path.exists(gz_fingerprint_file):
                    os.utime(gz_fingerprint_file, None)
                    with contextlib.closing(gzip_open(gz_fingerprint_file, 'rb')) as g:
                        with contextlib.closing(open(c_file, 'wb')) as f:
                            shutil.copyfileobj(g, f)
                else:
                    os.utime(zip_fingerprint_file, None)
                    dirname = os.path.dirname(c_file)
                    with contextlib.closing(zipfile.ZipFile(zip_fingerprint_file)) as z:
                        for artifact in z.namelist():
                            z.extract(artifact, os.path.join(dirname, artifact))
                return
        if not quiet:
            print("%sCythonizing %s" % (progress, pyx_file))
        if options is None:
            options = CompilationOptions(default_options)
        options.output_file = c_file
        options.embedded_metadata = embedded_metadata
    
        any_failures = 0
        try:
            result = compile_single(pyx_file, options, full_module_name=full_module_name)
            if result.num_errors > 0:
                any_failures = 1
        except (EnvironmentError, PyrexError) as e:
            sys.stderr.write('%s\n' % e)
            any_failures = 1
            # XXX
            import traceback
            traceback.print_exc()
        except Exception:
            if raise_on_failure:
                raise
            import traceback
            traceback.print_exc()
            any_failures = 1
        if any_failures:
            if raise_on_failure:
>               raise CompileError(None, pyx_file)
E               Cython.Compiler.Errors.CompileError: /usr/lib/python3.8/site-packages/numpy/random/_examples/cython/extending.pyx

CompileError = <class 'Cython.Compiler.Errors.CompileError'>
PyrexError = <class 'Cython.Compiler.Errors.PyrexError'>
any_failures = 1
c_file     = '/usr/lib/python3.8/site-packages/numpy/random/_examples/cython/extending.c'
compile_single = <function compile_single at 0x7f884fef6310>
default_options = {'annotate': None, 'annotate_coverage_xml': None, 'build_dir': None, 'c_line_in_traceback': True, ...}
embedded_metadata = {'distutils': {'depends': ['/usr/lib/python3.8/site-packages/numpy/core/include/numpy/arrayobject.h', '/usr/lib/python...ources': ['/usr/lib/python3.8/site-packages/numpy/random/_examples/cython/extending.pyx']}, 'module_name': 'extending'}
fingerprint = None
full_module_name = 'extending'
options    = <Cython.Compiler.Main.CompilationOptions object at 0x7f88540d5fa0>
progress   = '[1/2] '
pyx_file   = '/usr/lib/python3.8/site-packages/numpy/random/_examples/cython/extending.pyx'
quiet      = False
raise_on_failure = True
traceback  = <module 'traceback' from '/usr/lib/python3.8/traceback.py'>

/usr/lib/python3.8/site-packages/Cython/Build/Dependencies.py:1219: CompileError
---------------------------------------------------------------------------------------- Captured stdout call ----------------------------------------------------------------------------------------
Compiling /usr/lib/python3.8/site-packages/numpy/random/_examples/cython/extending.pyx because it changed.
Compiling /usr/lib/python3.8/site-packages/numpy/random/_examples/cython/extending_distributions.pyx because it changed.
[1/2] Cythonizing /usr/lib/python3.8/site-packages/numpy/random/_examples/cython/extending.pyx
---------------------------------------------------------------------------------------- Captured stderr call ----------------------------------------------------------------------------------------
[Errno 13] Permission denied: '/usr/lib/python3.8/site-packages/numpy/random/_examples/cython/extending.c'
Traceback (most recent call last):
  File "/usr/lib/python3.8/site-packages/Cython/Build/Dependencies.py", line 1202, in cythonize_one
    result = compile_single(pyx_file, options, full_module_name=full_module_name)
  File "/usr/lib/python3.8/site-packages/Cython/Compiler/Main.py", line 727, in compile_single
    return run_pipeline(source, options, full_module_name)
  File "/usr/lib/python3.8/site-packages/Cython/Compiler/Main.py", line 515, in run_pipeline
    err, enddata = Pipeline.run_pipeline(pipeline, source)
  File "/usr/lib/python3.8/site-packages/Cython/Compiler/Pipeline.py", line 355, in run_pipeline
    data = run(phase, data)
  File "/usr/lib/python3.8/site-packages/Cython/Compiler/Pipeline.py", line 335, in run
    return phase(data)
  File "/usr/lib/python3.8/site-packages/Cython/Compiler/Pipeline.py", line 52, in generate_pyx_code_stage
    module_node.process_implementation(options, result)
  File "/usr/lib/python3.8/site-packages/Cython/Compiler/ModuleNode.py", line 143, in process_implementation
    self.generate_c_code(env, options, result)
  File "/usr/lib/python3.8/site-packages/Cython/Compiler/ModuleNode.py", line 411, in generate_c_code
    f = open_new_file(result.c_file)
  File "/usr/lib/python3.8/site-packages/Cython/Utils.py", line 75, in open_new_file
    return codecs.open(path, "w", encoding="ISO-8859-1")
  File "/usr/lib/python3.8/codecs.py", line 905, in open
    file = builtins.open(filename, mode, buffering)
PermissionError: [Errno 13] Permission denied: '/usr/lib/python3.8/site-packages/numpy/random/_examples/cython/extending.c'
2 failed, 10399 passed, 85 skipped, 17 xfailed in 340.77s (0:05:40)
```


### Numpy/Python version information:

<!-- Output from 'import sys, numpy; print(numpy.__version__, sys.version)' -->
1.18.0 3.8.0 (default, Oct 23 2019, 18:51:26) 
[GCC 9.2.0]

Hints from issue discussion:
@mattip looks like `test_cython` needs to use a temporary directory.
@vkhodygo The test is new, so you can ignore it until it gets fixed. Thanks for the report.
@mattip we should also check the Cython version since 0.29.14 is needed for Python 3.8.
I think there is a whole potential can of worms in making the cython test public.
Copying `_examples/cython/` to a tempdir and executing `python setup.py build_ext -i` seems like the better way to do it anyway, the `sys.argv` manipulation looks suspicious. The current version will also leave build artifacts behind.
Side comment on that file: the indentation of the numba and cffi tests is off.

Commit message: TST, MAINT: skip on win32, fix formatting

Patch to review:
diff --git a/numpy/random/_examples/cython/setup.py b/numpy/random/_examples/cython/setup.py
index 19f045fc00b5..7ed0a3a18a99 100644
--- a/numpy/random/_examples/cython/setup.py
+++ b/numpy/random/_examples/cython/setup.py
@@ -12,6 +12,7 @@
 from os.path import join, abspath, dirname
 
 path = abspath(dirname(__file__))
+defs = [('NPY_NO_DEPRECATED_API', 0)]
 
 extending = Extension("extending",
                       sources=[join(path, 'extending.pyx')],
@@ -19,10 +20,13 @@
                             np.get_include(),
                             join(path, '..', '..')
                         ],
+                      define_macros=defs,
                       )
 distributions = Extension("extending_distributions",
                           sources=[join(path, 'extending_distributions.pyx')],
-                          include_dirs=[np.get_include()])
+                          include_dirs=[np.get_include()],
+                          define_macros=defs,
+                         )
 
 extensions = [extending, distributions]
 
diff --git a/numpy/random/tests/test_extending.py b/numpy/random/tests/test_extending.py
index 807de1a25164..87d4d37c496b 100644
--- a/numpy/random/tests/test_extending.py
+++ b/numpy/random/tests/test_extending.py
@@ -1,6 +1,8 @@
 import os, sys
 import pytest
 import warnings
+import shutil
+import subprocess
 
 try:
     import cffi
@@ -25,27 +27,29 @@
 except ImportError:
     cython = None
 
+cython_ver = cython.__version__.split('.')
+if len(cython_ver) < 3 or cython_ver < ['0', '29', '14']:
+    # too old or wrong cython, skip the test
+    cython = None
+
 @pytest.mark.skipif(cython is None, reason="requires cython")
-def test_cython():
+@pytest.mark.slow
+@pytest.mark.skipif(sys.platform == 'win32', reason="cmd too long on CI")
+def test_cython(tmp_path):
     curdir = os.getcwd()
     argv = sys.argv
-    examples = (os.path.dirname(__file__), '..', '_examples')
-    try:
-        os.chdir(os.path.join(*examples))
-        sys.argv = argv[:1] + ['build']
-        with warnings.catch_warnings(record=True) as w:
-            # setuptools issue gh-1885
-            warnings.filterwarnings('always', '', DeprecationWarning)
-            from numpy.random._examples.cython import setup
-    finally:
-        sys.argv = argv
-        os.chdir(curdir)
+    examples = os.path.join(os.path.dirname(__file__), '..', '_examples')
+    base = os.path.dirname(examples)
+    shutil.copytree(examples, tmp_path / '_examples')
+    env = os.environ.copy()
+    subprocess.check_call([sys.executable, 'setup.py', 'build'], env=env,
+                          cwd=str(tmp_path / '_examples' / 'cython'))
 
 @pytest.mark.skipif(numba is None or cffi is None,
                     reason="requires numba and cffi")
 def test_numba():
-        from numpy.random._examples.numba import extending
+    from numpy.random._examples.numba import extending
 
 @pytest.mark.skipif(cffi is None, reason="requires cffi")
 def test_cffi():
-        from numpy.random._examples.cffi import extending
+    from numpy.random._examples.cffi import extending


Code review comments:
diff_hunk: @@ -25,27 +27,29 @@
 except ImportError:
     cython = None
 
+cython_ver = cython.__version__.split('.')
path: numpy/random/tests/test_extending.py
text: @user1:
This should go within an `else` of the try/except above, else will raise AttributeError.

diff_hunk: @@ -25,27 +27,29 @@
 except ImportError:
     cython = None
 
+cython_ver = cython.__version__.split('.')
+if len(cython_ver) < 3 or cython_ver < ['0', '29', '14']:
+    # too old or wrong cython, skip the test
+    cython = None
+
 @pytest.mark.skipif(cython is None, reason="requires cython")
-def test_cython():
+@pytest.mark.slow
+@pytest.mark.skipif(sys.platform == 'win32', reason="cmd too long on CI")
+def test_cython(tmp_path):
     curdir = os.getcwd()
     argv = sys.argv
path: numpy/random/tests/test_extending.py
text: @user1:
These two lines can be deleted now.