Repository: cython/cython
Pull request: Make `Shadow.inline()` caching account for language version and compilation environment.
Closes #3419.

Problem statement:
Caching for `cython.inline()` does not differentiate between different compiler configurations.
```python3
>>> cython.inline('def f(int a, int b):\n\treturn a/b\nreturn f', language_level=3)(5,2)
2.5
```

```python3
>>> cython.inline('def f(int a, int b):\n\treturn a/b\nreturn f', language_level=2)(5,2)
2
>>> cython.inline('def f(int a, int b):\n\treturn a/b\nreturn f', language_level=3)(5,2)
2
```

Hints from issue discussion:
Interesting. I had added the `language_level` to the module hash, but it seems to have no effect.

https://github.com/cython/cython/blob/0b81c765bf9b23138b83311e07aacec74517e778/Cython/Build/Inline.py#L193-L194

I think that hash is being skipped entirely by the fast path at the very start of the function, which only takes into account the code string and (unbound?) argument signature:

https://github.com/cython/cython/blob/0b81c765bf9b23138b83311e07aacec74517e778/Cython/Build/Inline.py#L153-L162

Commit message: Update inlinecode.pyx

Patch to review:
diff --git a/Cython/Build/Inline.py b/Cython/Build/Inline.py
index cbcc5822dcc..5257ada18ce 100644
--- a/Cython/Build/Inline.py
+++ b/Cython/Build/Inline.py
@@ -141,6 +141,10 @@ def _populate_unbound(kwds, unbound_symbols, locals=None, globals=None):
             else:
                 print("Couldn't find %r" % symbol)
 
+def _inline_key(orig_code, arg_sigs, language_level):
+    key = orig_code, arg_sigs, sys.version_info, sys.executable, language_level, Cython.__version__
+    return hashlib.sha1(_unicode(key).encode('utf-8')).hexdigest()
+
 def cython_inline(code, get_type=unsafe_type,
                   lib_dir=os.path.join(get_cython_cache_dir(), 'inline'),
                   cython_include_dirs=None, cython_compiler_directives=None,
@@ -150,13 +154,20 @@ def cython_inline(code, get_type=unsafe_type,
         get_type = lambda x: 'object'
     ctx = _create_context(tuple(cython_include_dirs)) if cython_include_dirs else _cython_inline_default_context
 
+    cython_compiler_directives = dict(cython_compiler_directives or {})
+    if language_level is None and 'language_level' not in cython_compiler_directives:
+        language_level = '3str'
+    if language_level is not None:
+        cython_compiler_directives['language_level'] = language_level
+
     # Fast path if this has been called in this session.
     _unbound_symbols = _cython_inline_cache.get(code)
     if _unbound_symbols is not None:
         _populate_unbound(kwds, _unbound_symbols, locals, globals)
         args = sorted(kwds.items())
         arg_sigs = tuple([(get_type(value, ctx), arg) for arg, value in args])
-        invoke = _cython_inline_cache.get((code, arg_sigs))
+        key_hash = _inline_key(code, arg_sigs, language_level)
+        invoke = _cython_inline_cache.get((code, arg_sigs, key_hash))
         if invoke is not None:
             arg_list = [arg[1] for arg in args]
             return invoke(*arg_list)
@@ -177,12 +188,6 @@ def cython_inline(code, get_type=unsafe_type,
             # Parsing from strings not fully supported (e.g. cimports).
             print("Could not parse code as a string (to extract unbound symbols).")
 
-    cython_compiler_directives = dict(cython_compiler_directives or {})
-    if language_level is None and 'language_level' not in cython_compiler_directives:
-        language_level = '3str'
-    if language_level is not None:
-        cython_compiler_directives['language_level'] = language_level
-
     cimports = []
     for name, arg in list(kwds.items()):
         if arg is cython_module:
@@ -190,8 +195,8 @@ def cython_inline(code, get_type=unsafe_type,
             del kwds[name]
     arg_names = sorted(kwds)
     arg_sigs = tuple([(get_type(kwds[arg], ctx), arg) for arg in arg_names])
-    key = orig_code, arg_sigs, sys.version_info, sys.executable, language_level, Cython.__version__
-    module_name = "_cython_inline_" + hashlib.sha1(_unicode(key).encode('utf-8')).hexdigest()
+    key_hash = _inline_key(orig_code, arg_sigs, language_level)
+    module_name = "_cython_inline_" + key_hash
 
     if module_name in sys.modules:
         module = sys.modules[module_name]
@@ -258,7 +263,7 @@ def __invoke(%(params)s):
 
         module = load_dynamic(module_name, module_path)
 
-    _cython_inline_cache[orig_code, arg_sigs] = module.__invoke
+    _cython_inline_cache[orig_code, arg_sigs, key_hash] = module.__invoke
     arg_list = [kwds[arg] for arg in arg_names]
     return module.__invoke(*arg_list)
 
diff --git a/tests/run/inlinecode.pyx b/tests/run/inlinecode.pyx
new file mode 100644
index 00000000000..df05ae09a82
--- /dev/null
+++ b/tests/run/inlinecode.pyx
@@ -0,0 +1,17 @@
+import cython
+
+_inline_divcode = '''\
+def f(int a, int b):
+    return a/b
+return f'''
+
+def inline_langversion(language_level, a=5, b=2):
+    """
+    >>> inline_langversion(2)
+    2
+    >>> inline_langversion(3)
+    2.5
+    """
+    # Caching for inline code didn't always respect language version.
+    # https://github.com/cython/cython/issues/3419
+    print(cython.inline(_inline_divcode, language_level=language_level, quiet=True)(a=a, b=b))


Code review comments:
diff_hunk: @@ -14,4 +14,4 @@ def inline_langversion(language_level, a=5, b=2):
     """
     # Caching for inline code didn't always respect language version.
     # https://github.com/cython/cython/issues/3419
-    print(Shadow.inline(_inline_divcode, language_level=language_level, quiet=True)(a=a, b=b))
+    print(cython.inline(_inline_divcode, language_level=language_level, quiet=True)(a=a, b=b))
path: tests/run/inlinecode.pyx
text: @author:
IIRC, I tried `cython.inline` at first, but it gets interpreted [as the C `inline` modifier](https://cython.readthedocs.io/en/latest/src/tutorial/pure.html#extension-types-and-cdef-functions) (and raises an error) when the file gets compiled as a `.pyx` test.

@user1:
Hmm, right. I think this should actually go into `Cython/Build/Tests/TestInline.py` and not a `.pyx` file.