Repository: Delgan/loguru
Pull request #1034 (2023-11-30 13:22:54)

enable ruff/RET and fixing `RET505` Unnecessary elif after return statement
As it seems Ruff is well used, I may suggest also enabling RET simplification and also fixing all raised issues of _`RET505` Unnecessary elif after return statement_

Borda: I think that the codecov is a false alarm as we just reduced the number of lines os if any were not covered before, now they are just sending the wrong signal...
Delgan: Hi @Borda, thanks for suggesting additional rules and implementing the fixes!

Regarding the Codecov failure: Loguru aims for 100% code coverage, and I think we could and should add a new unit test to maintain this standard.

[One new line](https://github.com/Delgan/loguru/pull/1034/files#diff-acab8b47700c70707c332aba6b5e85925db903d112d5a148251ce791545d0d6cR27) was added but is not covered.

Would you mind adding an unit test in [this file](https://github.com/Delgan/loguru/blob/0f0cd6172a953e295fe941b65bb49d1649e46b4d/tests/test_defaults.py) such as `test_invalid_type()` for example, and ensuring the `env()` function returns `None` in this case?

On a second thought, I also think we shouldn't return `None` but raise an error instead:

```python
raise ValueError("The requested type '%r' is not supported" % type_)
```

Patch:
diff --git a/loguru/_colorizer.py b/loguru/_colorizer.py
index ed87cdb..18c072b 100644
--- a/loguru/_colorizer.py
+++ b/loguru/_colorizer.py
@@ -238,10 +238,9 @@ class AnsiParser:
                     self._tokens.append((TokenType.CLOSING, "\033[0m"))
                     self._tokens.extend(self._color_tokens)
                     continue
-                elif tag in self._tags:
+                if tag in self._tags:
                     raise ValueError('Closing tag "%s" violates nesting rules' % markup)
-                else:
-                    raise ValueError('Closing tag "%s" has no corresponding opening tag' % markup)
+                raise ValueError('Closing tag "%s" has no corresponding opening tag' % markup)
 
             if tag in {"lvl", "level"}:
                 token = (TokenType.LEVEL, None)
@@ -280,29 +279,29 @@ class AnsiParser:
         # Substitute on a direct match.
         if tag in style:
             return style[tag]
-        elif tag in foreground:
+        if tag in foreground:
             return foreground[tag]
-        elif tag in background:
+        if tag in background:
             return background[tag]
 
         # An alternative syntax for setting the color (e.g. <fg red>, <bg red>).
-        elif tag.startswith("fg ") or tag.startswith("bg "):
+        if tag.startswith("fg ") or tag.startswith("bg "):
             st, color = tag[:2], tag[3:]
             code = "38" if st == "fg" else "48"
 
             if st == "fg" and color.lower() in foreground:
                 return foreground[color.lower()]
-            elif st == "bg" and color.upper() in background:
+            if st == "bg" and color.upper() in background:
                 return background[color.upper()]
-            elif color.isdigit() and int(color) <= 255:
+            if color.isdigit() and int(color) <= 255:
                 return "\033[%s;5;%sm" % (code, color)
-            elif re.match(r"#(?:[a-fA-F0-9]{3}){1,2}$", color):
+            if re.match(r"#(?:[a-fA-F0-9]{3}){1,2}$", color):
                 hex_color = color[1:]
                 if len(hex_color) == 3:
                     hex_color *= 2
                 rgb = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
                 return "\033[%s;2;%s;%s;%sm" % ((code,) + rgb)
-            elif color.count(",") == 2:
+            if color.count(",") == 2:
                 colors = tuple(color.split(","))
                 if all(x.isdigit() and int(x) <= 255 for x in colors):
                     return "\033[%s;2;%s;%s;%sm" % ((code,) + colors)
diff --git a/loguru/_ctime_functions.py b/loguru/_ctime_functions.py
index e232b42..a5e345a 100644
--- a/loguru/_ctime_functions.py
+++ b/loguru/_ctime_functions.py
@@ -19,7 +19,7 @@ def load_ctime_functions():
 
         return get_ctime_windows, set_ctime_windows
 
-    elif hasattr(os.stat_result, "st_birthtime"):
+    if hasattr(os.stat_result, "st_birthtime"):
 
         def get_ctime_macos(filepath):
             return os.stat(filepath).st_birthtime
@@ -29,7 +29,7 @@ def load_ctime_functions():
 
         return get_ctime_macos, set_ctime_macos
 
-    elif hasattr(os, "getxattr") and hasattr(os, "setxattr"):
+    if hasattr(os, "getxattr") and hasattr(os, "setxattr"):
 
         def get_ctime_linux(filepath):
             try:
diff --git a/loguru/_defaults.py b/loguru/_defaults.py
index d3d8de7..5456e81 100644
--- a/loguru/_defaults.py
+++ b/loguru/_defaults.py
@@ -9,7 +9,7 @@ def env(key, type_, default=None):
 
     if type_ == str:
         return val
-    elif type_ == bool:
+    if type_ == bool:
         if val.lower() in ["1", "true", "yes", "y", "ok", "on"]:
             return True
         if val.lower() in ["0", "false", "no", "n", "nok", "off"]:
@@ -17,13 +17,14 @@ def env(key, type_, default=None):
         raise ValueError(
             "Invalid environment variable '%s' (expected a boolean): '%s'" % (key, val)
         )
-    elif type_ == int:
+    if type_ == int:
         try:
             return int(val)
         except ValueError:
             raise ValueError(
                 "Invalid environment variable '%s' (expected an integer): '%s'" % (key, val)
             ) from None
+    raise ValueError("The requested type '%r' is not supported" % type_)
 
 
 LOGURU_AUTOINIT = env("LOGURU_AUTOINIT", bool, True)
diff --git a/loguru/_file_sink.py b/loguru/_file_sink.py
index bdc6ccd..4b09b06 100644
--- a/loguru/_file_sink.py
+++ b/loguru/_file_sink.py
@@ -310,7 +310,7 @@ class FileSink:
     def _make_rotation_function(rotation):
         if rotation is None:
             return None
-        elif isinstance(rotation, str):
+        if isinstance(rotation, str):
             size = string_parsers.parse_size(rotation)
             if size is not None:
                 return FileSink._make_rotation_function(size)
@@ -330,45 +330,41 @@ class FileSink:
                 step_forward = partial(Rotation.forward_weekday, weekday=day)
                 return Rotation.RotationTime(step_forward, time)
             raise ValueError("Cannot parse rotation from: '%s'" % rotation)
-        elif isinstance(rotation, (numbers.Real, decimal.Decimal)):
+        if isinstance(rotation, (numbers.Real, decimal.Decimal)):
             return partial(Rotation.rotation_size, size_limit=rotation)
-        elif isinstance(rotation, datetime.time):
+        if isinstance(rotation, datetime.time):
             return Rotation.RotationTime(Rotation.forward_day, rotation)
-        elif isinstance(rotation, datetime.timedelta):
+        if isinstance(rotation, datetime.timedelta):
             step_forward = partial(Rotation.forward_interval, interval=rotation)
             return Rotation.RotationTime(step_forward)
-        elif callable(rotation):
+        if callable(rotation):
             return rotation
-        else:
-            raise TypeError(
-                "Cannot infer rotation for objects of type: '%s'" % type(rotation).__name__
-            )
+        raise TypeError("Cannot infer rotation for objects of type: '%s'" % type(rotation).__name__)
 
     @staticmethod
     def _make_retention_function(retention):
         if retention is None:
             return None
-        elif isinstance(retention, str):
+        if isinstance(retention, str):
             interval = string_parsers.parse_duration(retention)
             if interval is None:
                 raise ValueError("Cannot parse retention from: '%s'" % retention)
             return FileSink._make_retention_function(interval)
-        elif isinstance(retention, int):
+        if isinstance(retention, int):
             return partial(Retention.retention_count, number=retention)
-        elif isinstance(retention, datetime.timedelta):
+        if isinstance(retention, datetime.timedelta):
             return partial(Retention.retention_age, seconds=retention.total_seconds())
-        elif callable(retention):
+        if callable(retention):
             return retention
-        else:
-            raise TypeError(
-                "Cannot infer retention for objects of type: '%s'" % type(retention).__name__
-            )
+        raise TypeError(
+            "Cannot infer retention for objects of type: '%s'" % type(retention).__name__
+        )
 
     @staticmethod
     def _make_compression_function(compression):
         if compression is None:
             return None
-        elif isinstance(compression, str):
+        if isinstance(compression, str):
             ext = compression.strip().lstrip(".")
 
             if ext == "gz":
@@ -426,9 +422,8 @@ class FileSink:
                 raise ValueError("Invalid compression format: '%s'" % ext)
 
             return partial(Compression.compression, ext="." + ext, compress_function=compress)
-        elif callable(compression):
+        if callable(compression):
             return compression
-        else:
-            raise TypeError(
-                "Cannot infer compression for objects of type: '%s'" % type(compression).__name__
-            )
+        raise TypeError(
+            "Cannot infer compression for objects of type: '%s'" % type(compression).__name__
+        )
diff --git a/loguru/_logger.py b/loguru/_logger.py
index f750967..ec9d284 100644
--- a/loguru/_logger.py
+++ b/loguru/_logger.py
@@ -1225,7 +1225,7 @@ class Logger:
 
             def __exit__(self, type_, value, traceback_):
                 if type_ is None:
-                    return
+                    return None
 
                 if not issubclass(type_, exception):
                     return False
@@ -1582,8 +1582,7 @@ class Logger:
                     "Level '%s' does not exist, you have to create it by specifying a level no"
                     % name
                 )
-            else:
-                old_color, old_icon = "", " "
+            old_color, old_icon = "", " "
         elif no is not None:
             raise TypeError("Level '%s' already exists, you can't update its severity no" % name)
         else:
diff --git a/loguru/_string_parsers.py b/loguru/_string_parsers.py
index 1e135ab..4fa67c1 100644
--- a/loguru/_string_parsers.py
+++ b/loguru/_string_parsers.py
@@ -51,9 +51,7 @@ def parse_size(size):
     u = "kmgtpezy".index(u.lower()) + 1 if u else 0
     i = 1024 if i else 1000
     b = {"b": 8, "B": 1}[b] if b else 1
-    size = s * i**u / b
-
-    return size
+    return s * i**u / b
 
 
 def parse_duration(duration):
@@ -118,7 +116,7 @@ def parse_day(day):
     day = day.strip().lower()
     if day in days:
         return days[day]
-    elif day.startswith("w") and day[1:].isdigit():
+    if day.startswith("w") and day[1:].isdigit():
         day = int(day[1:])
         if not 0 <= day < 7:
             raise ValueError("Invalid weekday value while parsing day (expected [0-6]): '%d'" % day)
diff --git a/pyproject.toml b/pyproject.toml
index 94ddbb9..87cbb24 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -18,7 +18,7 @@ testpaths = ["tests"]
 exclude = ["tests/exceptions/source/*"]
 line-length = 100
 # Enforce pyflakes(F), pycodestyle(E, W), isort (I), bugbears (B), and pep8-naming (N) rules.
-select = ["F", "E", "W", "I", "B", "N"]
+select = ["F", "E", "W", "I", "B", "N", "RET"]
 
 [tool.ruff.pycodestyle]
 max-doc-length = 100
diff --git a/setup.py b/setup.py
index 6acae14..2f84623 100644
--- a/setup.py
+++ b/setup.py
@@ -79,11 +79,11 @@ setup(
             "mypy==v0.910 ; python_version<'3.6'",
             "mypy==v0.971 ; python_version>='3.6' and python_version<'3.7'",
             "mypy==v1.4.1 ; python_version>='3.7' and python_version<'3.8'",
-            "mypy==v1.6.1 ; python_version>='3.8'",
+            "mypy==v1.7.1 ; python_version>='3.8'",
             # Docs.
             "Sphinx==7.2.5 ; python_version>='3.9'",
             "sphinx-autobuild==2021.3.14 ; python_version>='3.9'",
-            "sphinx-rtd-theme==1.3.0 ; python_version>='3.9'",
+            "sphinx-rtd-theme==2.0.0 ; python_version>='3.9'",
         ]
     },
     python_requires=">=3.5",


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index ff0191b..60442bb 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -54,8 +54,7 @@ def parse(text, *, strip=False, strict=True):
 
     if strip:
         return parser.strip(tokens)
-    else:
-        return parser.colorize(tokens, "")
+    return parser.colorize(tokens, "")
 
 
 def check_dir(dir, *, files=None, size=None):
diff --git a/tests/test_add_option_catch.py b/tests/test_add_option_catch.py
index 9551f70..5bf1b41 100644
--- a/tests/test_add_option_catch.py
+++ b/tests/test_add_option_catch.py
@@ -115,8 +115,7 @@ def test_broken_sink_caught_keep_working(enqueue):
         nonlocal output
         if m.startswith("NOK"):
             raise ValueError("Broken!")
-        else:
-            output += m
+        output += m
 
     logger.add(half_broken_sink, format="{message}", enqueue=enqueue, catch=True)
     logger.info("A")
diff --git a/tests/test_defaults.py b/tests/test_defaults.py
index c8d039c..148ffe2 100644
--- a/tests/test_defaults.py
+++ b/tests/test_defaults.py
@@ -50,3 +50,11 @@ def test_invalid_bool(value, monkeypatch):
         context.setenv(key, value)
         with pytest.raises(ValueError):
             env(key, bool)
+
+
+def test_invalid_type(monkeypatch):
+    with monkeypatch.context() as context:
+        key = "INVALID_TYPE"
+        context.setenv(key, 42.0)
+        with pytest.raises(ValueError):
+            env(key, float)
diff --git a/tests/typesafety/test_logger.yml b/tests/typesafety/test_logger.yml
index 3815a7e..8e4ae13 100644
--- a/tests/typesafety/test_logger.yml
+++ b/tests/typesafety/test_logger.yml
@@ -194,17 +194,19 @@
     main:5: note: Revealed type is "loguru.Contextualizer"
 
 - case: level_get
-  skip: sys.version_info < (3, 9)
+  skip: sys.version_info < (3, 7)
   main: |
     from loguru import logger
     import loguru
     level = logger.level("INFO")
     reveal_type(level)
   out: |
-    main:4: note: Revealed type is "tuple[builtins.str, builtins.int, builtins.str, builtins.str, fallback=loguru.Level]"
+    main:4: note: Revealed type is "Tuple[builtins.str, builtins.int, builtins.str, builtins.str, fallback=loguru.Level]"
+  mypy_config: |
+    force_uppercase_builtins = true
 
-- case: level_get_pre39
-  skip: not sys.version_info < (3, 9)
+- case: level_get_pre37
+  skip: not sys.version_info < (3, 7)
   main: |
     from loguru import logger
     import loguru
@@ -214,17 +216,19 @@
     main:4: note: Revealed type is "Tuple[builtins.str, builtins.int, builtins.str, builtins.str, fallback=loguru.Level]"
 
 - case: level_set
-  skip: sys.version_info < (3, 9)
+  skip: sys.version_info < (3, 7)
   main: |
     from loguru import logger
     import loguru
     level = logger.level("FOO", no=11, icon="!", color="<blue>")
     reveal_type(level)
   out: |
-    main:4: note: Revealed type is "tuple[builtins.str, builtins.int, builtins.str, builtins.str, fallback=loguru.Level]"
+    main:4: note: Revealed type is "Tuple[builtins.str, builtins.int, builtins.str, builtins.str, fallback=loguru.Level]"
+  mypy_config: |
+    force_uppercase_builtins = true
 
-- case: level_set_pre39
-  skip: not sys.version_info < (3, 9)
+- case: level_set_pre37
+  skip: not sys.version_info < (3, 7)
   main: |
     from loguru import logger
     import loguru
@@ -234,17 +238,19 @@
     main:4: note: Revealed type is "Tuple[builtins.str, builtins.int, builtins.str, builtins.str, fallback=loguru.Level]"
 
 - case: level_update
-  skip: sys.version_info < (3, 9)
+  skip: sys.version_info < (3, 7)
   main: |
     from loguru import logger
     import loguru
     level = logger.level("INFO", color="<blue>")
     reveal_type(level)
   out: |
-    main:4: note: Revealed type is "tuple[builtins.str, builtins.int, builtins.str, builtins.str, fallback=loguru.Level]"
+    main:4: note: Revealed type is "Tuple[builtins.str, builtins.int, builtins.str, builtins.str, fallback=loguru.Level]"
+  mypy_config: |
+    force_uppercase_builtins = true
 
-- case: level_update_pre39
-  skip: not sys.version_info < (3, 9)
+- case: level_update_pre37
+  skip: not sys.version_info < (3, 7)
   main: |
     from loguru import logger
     import loguru
@@ -286,35 +292,23 @@
     main:6: note: Revealed type is "builtins.dict[builtins.str, Any]"
 
 - case: invalid_add_argument
-  skip: sys.version_info < (3, 10)
-  main: |
-    from loguru import logger
-    logger.add(lambda m: None, foobar=123)
-  out: |
-    main:2: error: No overload variant of "add" of "Logger" matches argument types "Callable[[Any], None]", "int"
-    main:2: note: Possible overload variants:
-    main:2: note:     def add(self, sink: TextIO | Writable | Callable[[Message], None] | Handler, *, level: str | int = ..., format: str | Callable[[Record], str] = ..., filter: str | Callable[[Record], bool] | dict[str | None, str | int | bool] | None = ..., colorize: bool | None = ..., serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., enqueue: bool = ..., context: str | BaseContext | None = ..., catch: bool = ...) -> int
-    main:2: note:     def add(self, sink: Callable[[Message], Awaitable[None]], *, level: str | int = ..., format: str | Callable[[Record], str] = ..., filter: str | Callable[[Record], bool] | dict[str | None, str | int | bool] | None = ..., colorize: bool | None = ..., serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., enqueue: bool = ..., context: str | BaseContext | None = ..., catch: bool = ..., loop: AbstractEventLoop | None = ...) -> int
-    main:2: note:     def add(self, sink: str | PathLike[str], *, level: str | int = ..., format: str | Callable[[Record], str] = ..., filter: str | Callable[[Record], bool] | dict[str | None, str | int | bool] | None = ..., colorize: bool | None = ..., serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., enqueue: bool = ..., context: str | BaseContext | None = ..., catch: bool = ..., rotation: str | int | time | timedelta | Callable[[Message, TextIO], bool] | None = ..., retention: str | int | timedelta | Callable[[list[str]], None] | None = ..., compression: str | Callable[[str], None] | None = ..., delay: bool = ..., watch: bool = ..., mode: str = ..., buffering: int = ..., encoding: str = ..., **kwargs: Any) -> int
-  mypy_config: |
-    show_error_codes = false
-
-- case: invalid_add_argument_pre310
-  skip: not (3, 9) <= sys.version_info < (3, 10)
+  skip: sys.version_info < (3, 7)
   main: |
     from loguru import logger
     logger.add(lambda m: None, foobar=123)
   out: |
     main:2: error: No overload variant of "add" of "Logger" matches argument types "Callable[[Any], None]", "int"
     main:2: note: Possible overload variants:
-    main:2: note:     def add(self, sink: Union[TextIO, Writable, Callable[[Message], None], Handler], *, level: Union[str, int] = ..., format: Union[str, Callable[[Record], str]] = ..., filter: Union[str, Callable[[Record], bool], dict[Optional[str], Union[str, int, bool]], None] = ..., colorize: Optional[bool] = ..., serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., enqueue: bool = ..., context: Union[str, BaseContext, None] = ..., catch: bool = ...) -> int
-    main:2: note:     def add(self, sink: Callable[[Message], Awaitable[None]], *, level: Union[str, int] = ..., format: Union[str, Callable[[Record], str]] = ..., filter: Union[str, Callable[[Record], bool], dict[Optional[str], Union[str, int, bool]], None] = ..., colorize: Optional[bool] = ..., serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., enqueue: bool = ..., context: Union[str, BaseContext, None] = ..., catch: bool = ..., loop: Optional[AbstractEventLoop] = ...) -> int
-    main:2: note:     def add(self, sink: Union[str, PathLike[str]], *, level: Union[str, int] = ..., format: Union[str, Callable[[Record], str]] = ..., filter: Union[str, Callable[[Record], bool], dict[Optional[str], Union[str, int, bool]], None] = ..., colorize: Optional[bool] = ..., serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., enqueue: bool = ..., context: Union[str, BaseContext, None] = ..., catch: bool = ..., rotation: Union[str, int, time, timedelta, Callable[[Message, TextIO], bool], None] = ..., retention: Union[str, int, timedelta, Callable[[list[str]], None], None] = ..., compression: Union[str, Callable[[str], None], None] = ..., delay: bool = ..., watch: bool = ..., mode: str = ..., buffering: int = ..., encoding: str = ..., **kwargs: Any) -> int
+    main:2: note:     def add(self, sink: Union[TextIO, Writable, Callable[[Message], None], Handler], *, level: Union[str, int] = ..., format: Union[str, Callable[[Record], str]] = ..., filter: Union[str, Callable[[Record], bool], Dict[Optional[str], Union[str, int, bool]], None] = ..., colorize: Optional[bool] = ..., serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., enqueue: bool = ..., context: Union[str, BaseContext, None] = ..., catch: bool = ...) -> int
+    main:2: note:     def add(self, sink: Callable[[Message], Awaitable[None]], *, level: Union[str, int] = ..., format: Union[str, Callable[[Record], str]] = ..., filter: Union[str, Callable[[Record], bool], Dict[Optional[str], Union[str, int, bool]], None] = ..., colorize: Optional[bool] = ..., serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., enqueue: bool = ..., context: Union[str, BaseContext, None] = ..., catch: bool = ..., loop: Optional[AbstractEventLoop] = ...) -> int
+    main:2: note:     def add(self, sink: Union[str, PathLike[str]], *, level: Union[str, int] = ..., format: Union[str, Callable[[Record], str]] = ..., filter: Union[str, Callable[[Record], bool], Dict[Optional[str], Union[str, int, bool]], None] = ..., colorize: Optional[bool] = ..., serialize: bool = ..., backtrace: bool = ..., diagnose: bool = ..., enqueue: bool = ..., context: Union[str, BaseContext, None] = ..., catch: bool = ..., rotation: Union[str, int, time, timedelta, Callable[[Message, TextIO], bool], None] = ..., retention: Union[str, int, timedelta, Callable[[List[str]], None], None] = ..., compression: Union[str, Callable[[str], None], None] = ..., delay: bool = ..., watch: bool = ..., mode: str = ..., buffering: int = ..., encoding: str = ..., **kwargs: Any) -> int
   mypy_config: |
     show_error_codes = false
+    force_uppercase_builtins = true
+    force_union_syntax = true
 
-- case: invalid_add_argument_pre39
-  skip: not sys.version_info < (3, 9)
+- case: invalid_add_argument_pre37
+  skip: not sys.version_info < (3, 7)
   main: |
     from loguru import logger
     logger.add(lambda m: None, foobar=123)
@@ -354,26 +348,7 @@
     show_error_codes = false
 
 - case: invalid_configuration
-  skip: sys.version_info < (3, 10)
-  main: |
-    from loguru import logger
-    logger.configure(
-      handlers=[{"x": "y"}],
-      levels=[{"baz": 1}],
-      patcher=123,
-      activation=[{"foo": "bar"}],
-      extra=[],
-    )
-  out: |
-    main:4: error: Extra key "baz" for TypedDict "LevelConfig"
-    main:5: error: Argument "patcher" to "configure" of "Logger" has incompatible type "int"; expected "Callable[[Record], None] | None"
-    main:6: error: List item 0 has incompatible type "dict[str, str]"; expected "tuple[str | None, bool]"
-    main:7: error: Argument "extra" to "configure" of "Logger" has incompatible type "list[<nothing>]"; expected "dict[Any, Any] | None"
-  mypy_config: |
-    show_error_codes = false
-
-- case: invalid_configuration_pre310
-  skip: not (3, 9) <= sys.version_info < (3, 10)
+  skip: sys.version_info < (3, 7)
   main: |
     from loguru import logger
     logger.configure(
@@ -381,18 +356,20 @@
       levels=[{"baz": 1}],
       patcher=123,
       activation=[{"foo": "bar"}],
-      extra=[],
+      extra=[1],
     )
   out: |
     main:4: error: Extra key "baz" for TypedDict "LevelConfig"
     main:5: error: Argument "patcher" to "configure" of "Logger" has incompatible type "int"; expected "Optional[Callable[[Record], None]]"
-    main:6: error: List item 0 has incompatible type "dict[str, str]"; expected "tuple[Optional[str], bool]"
-    main:7: error: Argument "extra" to "configure" of "Logger" has incompatible type "list[<nothing>]"; expected "Optional[dict[Any, Any]]"
+    main:6: error: List item 0 has incompatible type "Dict[str, str]"; expected "Tuple[Optional[str], bool]"
+    main:7: error: Argument "extra" to "configure" of "Logger" has incompatible type "List[int]"; expected "Optional[Dict[Any, Any]]"
   mypy_config: |
     show_error_codes = false
+    force_uppercase_builtins = true
+    force_union_syntax = true
 
-- case: invalid_configuration_pre39
-  skip: not sys.version_info < (3, 9)
+- case: invalid_configuration_pre37
+  skip: not sys.version_info < (3, 7)
   main: |
     from loguru import logger
     logger.configure(
@@ -400,12 +377,12 @@
       levels=[{"baz": 1}],
       patcher=123,
       activation=[{"foo": "bar"}],
-      extra=[],
+      extra=[1],
     )
   out: |
     main:4: error: Extra key "baz" for TypedDict "LevelConfig"
     main:5: error: Argument "patcher" to "configure" of "Logger" has incompatible type "int"; expected "Optional[Callable[[Record], None]]"
     main:6: error: List item 0 has incompatible type "Dict[str, str]"; expected "Tuple[Optional[str], bool]"
-    main:7: error: Argument "extra" to "configure" of "Logger" has incompatible type "List[<nothing>]"; expected "Optional[Dict[Any, Any]]"
+    main:7: error: Argument "extra" to "configure" of "Logger" has incompatible type "List[int]"; expected "Optional[Dict[Any, Any]]"
   mypy_config: |
     show_error_codes = false