Repository: cython/cython
Pull request: Dont override language_level from directives when set to None
Fixes #5484


Problem statement:
[BUG] -3 does not set language level in Cython 3
### Describe the bug

The -3 flag does not set language level to 3.

When compiling some code with the ``always_allow_keywords`` directive set to true with Cython 3, Cython doesn't set the language level to 3 even though ``-3`` is passed, and infers the language level as ``3str`` causing the compile failure.

See logs I have attached below.

### Code to reproduce the behaviour:

``` 
cython -M --fast-fail -3 --include-dir /home/runner/work/pandas/pandas/build/cp310/pandas/_libs/tslibs '-X always_allow_keywords=true' /home/runner/work/pandas/pandas/pandas/_libs/tslibs/offsets.pyx -o pandas/_libs/tslibs/offsets.cpython-310-x86_64-linux-gnu.so.p/pandas/_libs/tslibs/offsets.pyx.c
  /home/runner/micromamba/envs/test/lib/python3.10/site-packages/Cython/Compiler/Main.py:370: FutureWarning: Cython directive 'language_level' not set, using '3str' for now (Py3). This has changed from earlier releases! File: /home/runner/work/pandas/pandas/pandas/_libs/tslibs/offsets.pxd
    tree = Parsing.p_module(s, pxd, full_module_name)

  Error compiling Cython file:
  ------------------------------------------------------------
  ...

      def get_rule_code_suffix(self) -> str:
          prefix = self._get_suffix_prefix()
          month = MONTH_ALIASES[self.startingMonth]
          weekday = int_to_weekday[self.weekday]
          return f"{prefix}-{month}-{weekday}"

```

### Expected behaviour

Language level should be inferred as 3

### OS

Ubuntu 22.04.2

### Python version

3.10.11

### Cython version

Cython 3.0.0b3

### Additional context

_No response_

Hints from issue discussion:
Thanks for the report. Can reproduce this. Just to make clear (since it wasn't 100% clear to me from the issue) it's the '-X ...' argument that seems to trigger it. A simple `cython -3 filename.pyx` looks to work fine.
Issue is caused by following lines:
https://github.com/cython/cython/blob/0605b260541394d7b33110d0f21830be507fde76/Cython/Compiler/Options.py#L654-L655
when `-X` switch is used, `directives` variable contains dictionary with directives containing also `language_level` set to `None`. Hence the `options` dictionary with key `language_level` is set to `None` instead of `3`.

Commit message: Fix wording

Patch to review:
diff --git a/Cython/Compiler/Options.py b/Cython/Compiler/Options.py
index e7b33fbc6e9..11862b95660 100644
--- a/Cython/Compiler/Options.py
+++ b/Cython/Compiler/Options.py
@@ -651,7 +651,7 @@ def __init__(self, defaults=None, **kw):
             import warnings
             warnings.warn("C++ mode forced when in Pythran mode!")
             options['cplus'] = True
-        if 'language_level' in directives and 'language_level' not in kw:
+        if directives.get('language_level', None) and 'language_level' not in kw:
             options['language_level'] = directives['language_level']
         elif not options.get('language_level'):
             options['language_level'] = directive_defaults.get('language_level')
diff --git a/tests/build/cython_language_level.srctree b/tests/build/cython_language_level.srctree
new file mode 100644
index 00000000000..26774964b3e
--- /dev/null
+++ b/tests/build/cython_language_level.srctree
@@ -0,0 +1,10 @@
+# The test is checking the combination of passing command-line compiler directives
+# and language level to make sure that language level is preserved. The test
+# is designed to fail when language_level isn't 3 due to fstring
+# being unicode and str being ambiguous.
+CYTHON -3 foo.pyx
+CYTHON -3 -X always_allow_keywords=true foo.pyx
+
+######## foo.pyx ########
+def join(a: str, b: str) -> str:
+    return f'{a}{b}'


Code review comments:
diff_hunk: @@ -651,7 +651,7 @@ def __init__(self, defaults=None, **kw):
             import warnings
             warnings.warn("C++ mode forced when in Pythran mode!")
             options['cplus'] = True
-        if 'language_level' in directives and 'language_level' not in kw:
+        if directives.get('language_level', None) and 'language_level' not in kw:
path: Cython/Compiler/Options.py
text: @user1:
We don't need the explicit `None` here. And checking for `in` is faster than calling `.get()`, so let's reverse the order.
```suggestion
        if 'language_level' not in kw and directives.get('language_level'):
```