Repository: kivy/kivy

Issue:
'Canvas.add()' checks boundary but  'Canvas.insert()' doesn't
**Software Versions**
* Python: 3.8.12
* OS: Linux Mint 20.2 Mate Edition
* Kivy: master 714f4935085d4ac0c220fdf7183fb35175f8bcad
* Kivy installation method: `git clone` then `python -m pip install -e ".[dev,full]"`

### Issue

There was a canvas containing a `before` group and an `after` group.

```python
from kivy.uix.widget import Widget

c = Widget().canvas

def print_children(c):
    print([i.__class__.__name__ for i in c.children])

print_children(c)
c.before
c.after
print_children(c)
```

```
[]
['CanvasBase', 'CanvasBase']
```

When  I called `c.insert(0, PushMatrix())` and `c.add(PopMatrix())`, I was expecting the `c.children` to be either of

- `['CanvasBase', 'PushMatrix', 'PopMatrix', 'CanvasBase']` or
- `['PushMatrix', 'CanvasBase', 'CanvasBase', 'PopMatrix']`

but neither was an actual result.

```python
c.insert(0, PushMatrix())
c.add(PopMatrix())
print_children(c)
```

```
['PushMatrix', 'CanvasBase', 'PopMatrix', 'CanvasBase']
```

### Cause

The cause of the issue is that `Canvas` overrides `InstructionGroup.add()` but doesn't  override `InstructionGroup.insert()`.
https://github.com/kivy/kivy/blob/714f4935085d4ac0c220fdf7183fb35175f8bcad/kivy/graphics/instructions.pyx#L656-L662

As you can see, `Canvas.add()` does some check so that the `after` group always remains the last one, which is why I couldn't add a PopMatrix behind the `after` group. On the other hand, `InstructionGroup.insert()` doesn't do that (Of course, `InstructionGroup` is not even aware of the existance of the `before` group and the `after` group),

https://github.com/kivy/kivy/blob/714f4935085d4ac0c220fdf7183fb35175f8bcad/kivy/graphics/instructions.pyx#L191-L195

which is why I was able to insert a PushMatrix in font of the `before` group.

### Solution

I think it's better to

- document about this unintuitive behavior or
- have `Canvas` override `insert()` and make it work in the same way as `add()`

### to reproduce the issue

```python
from kivy.graphics import Canvas, PushMatrix, PopMatrix
from kivy.uix.widget import Widget


def print_children(c: Canvas):
    print([i.__class__.__name__ for i in c.children])

c = Widget().canvas

print("\n---- initial state ----")
print_children(c)

c.before
c.after
print("\n---- added 'after' and 'before' ----")
print_children(c)

c.add(PopMatrix())
print("\n---- after c.add(PopMatrix()) ----")
print_children(c)

c.insert(0, PushMatrix())
print("\n---- after c.insert(0, PushMatrix()) ----")
print_children(c)
```

Discussion:
gottadiveintopython: @tshirtman  Thanks for the approval. I might edit the doc later because I'm not confident in my English so don't merge yet. And it would be great if you give me an advice about this pr.
misl6: @gottadiveintopython

In case you want to have a green check (I've seen your reaction on the PEP8 PR which I completely forgot about), feel free to force-push as it's now merged.
gottadiveintopython: @misl6 Thanks, I'll do that later.

Patch:
diff --git a/doc/autobuild.py b/doc/autobuild.py
index 1c9222003..cd3f311cd 100644
--- a/doc/autobuild.py
+++ b/doc/autobuild.py
@@ -15,7 +15,8 @@ ignore_list = (
     'kivy.graphics.vertex',
     'kivy.uix.recycleview.__init__',
     'kivy.setupconfig',
-    'kivy.version'
+    'kivy.version',
+    'kivy._version'
 )
 
 import os
@@ -241,7 +242,7 @@ refid = 0
 for module in m:
     summary = extract_summary_line(sys.modules[module].__doc__)
     if summary is None or summary == '':
-        summary = 'NO DOCUMENTATION (module %s)' % package
+        summary = 'NO DOCUMENTATION (module %s)' % module
 
     # search examples
     example_output = []
diff --git a/kivy/graphics/instructions.pyx b/kivy/graphics/instructions.pyx
index 43d05f8a2..c1f1ab20c 100644
--- a/kivy/graphics/instructions.pyx
+++ b/kivy/graphics/instructions.pyx
@@ -654,6 +654,11 @@ cdef class Canvas(CanvasBase):
         return 0
 
     cpdef add(self, Instruction c):
+        '''Append an :class:`Instruction` to our list. If the canvas contains
+        an `after` group, then this instruction is inserted just before the
+        after group, which remains last. This is different from how
+        :meth:`insert` works, which can insert anywhere.
+        '''
         # the after group must remain the last one.
         if self._after is None:
             c.radd(self)
diff --git a/kivy/logger.py b/kivy/logger.py
index b9d664c1b..4b1d32c20 100644
--- a/kivy/logger.py
+++ b/kivy/logger.py
@@ -133,6 +133,7 @@ COLORS = {
     'CRITICAL': RED,
     'ERROR': RED}
 
+logging.addLevelName(9, 'TRACE')
 logging.TRACE = 9
 LOG_LEVELS = {
     'trace': logging.TRACE,
@@ -296,9 +297,6 @@ class ColoredFormatter(logging.Formatter):
         except:
             pass
         levelname = record.levelname
-        if record.levelno == logging.TRACE:
-            levelname = 'TRACE'
-            record.levelname = levelname
         if self.use_color and levelname in COLORS:
             levelname_color = (
                 COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ)


Test patch:
diff --git a/kivy/tests/test_logger.py b/kivy/tests/test_logger.py
index 90269b126..0a6198dbf 100644
--- a/kivy/tests/test_logger.py
+++ b/kivy/tests/test_logger.py
@@ -58,3 +58,30 @@ def test_purge_logs(tmp_path, file_handler, n):
             expected_names = expected_names[:-1]
 
     assert set(expected_names) == files
+
+
+def test_trace_level():
+    """Kivy logger defines a custom level of Trace."""
+    from kivy.logger import Logger, LOG_LEVELS, LoggerHistory
+    import logging
+
+    Logger.setLevel(9)
+    # Try different ways to trigger a trace:
+    Logger.trace("test: This is trace message 1")
+    logging.log(logging.TRACE, "test: This is trace message 2")
+    Logger.log(LOG_LEVELS["trace"], "test: This is trace message 3")
+    # Not supported:
+    # logging.trace('test: This is trace message 4')
+
+    last_log_records = LoggerHistory.history[:3]
+    assert all(log_record.levelno == 9 for log_record in last_log_records), [
+        log_record.levelno for log_record in last_log_records
+    ]
+
+
+def test_trace_level_has_level_name():
+    from kivy.logger import Logger, LoggerHistory
+
+    Logger.setLevel(9)
+    Logger.trace("test: This is trace message 1")
+    assert LoggerHistory.history[0].levelname == "TRACE"