Repository: napari/napari

Issue:
Deprecated `edge_color` cannot be passed to `ViewerModel.add_points`
### 🐛 Bug Report

I tried to pass `edge_color` through `ViewerModel.add_points`. I expected to get a deprecation warning, but instead I got an exception:

```
TypeError: add_points() got an unexpected keyword argument 'edge_color'
```

Note that I can pass `edge_color` to the `Points.__init__`.

I assume similar behavior for other deprecated `edge_*` parameters, but I have not tried them. 

### 💡 Steps to Reproduce

```python
from napari.layers import Points
p = Points(edge_color='red')  # Works with deprecation warning
v = napari.components.ViewerModel()
v.add_points(edge_color='red')  # Fails
```

### 💡 Expected Behavior

I expect to get the same deprecation warning as when using `Points.__init__`.

### 🌎 Environment

napari: 0.4.17rc4.dev907+g3aac4b71
Platform: macOS-14.5-arm64-arm-64bit
System: MacOS 14.5
Python: 3.10.14 | packaged by conda-forge | (main, Mar 20 2024, 12:51:49) [Clang 16.0.6 ]
Qt: 5.15.11
PyQt5: 5.15.10
NumPy: 1.26.4
SciPy: 1.13.1
Dask: 2024.5.1
VisPy: 0.14.2
magicgui: 0.8.2
superqt: 0.6.6
in-n-out: 0.2.1
app-model: 0.2.7
npe2: 0.7.5

OpenGL:
  - GL version:  2.1 Metal - 88.1
  - MAX_TEXTURE_SIZE: 16384
  - GL_MAX_3D_TEXTURE_SIZE: 2048

Screens:
  - screen 1: resolution 3440x1440, scale 1.0
  - screen 2: resolution 1512x982, scale 2.0

Optional:
  - numba: 0.59.1
  - triangle not installed

Settings path:
  - /Users/asweet/Library/Application Support/napari/napari-dev_bed8b9b3d61b11a9560a577ec715d63283498f35/settings.yaml
Plugins:
  - napari: 0.4.8rc3.dev2013+g2300b0ed (81 contributions)
  - napari-console: 0.0.9 (0 contributions)
  - napari-svg: 0.1.10 (2 contributions)

### 💡 Additional Context

I expect this bug occurs because `add_points` is automatically generated using the signature of `Points.__init__`, but `rename_argument` works in a way that does not add the deprecated name to the signature (instead it assigns the deprecated parameter value to the new parameter).
PyCharm debugging leads to `TypeError` if `add_xxx()` is called
### 🐛 Bug Report

I used to be able to use the debugger in PyCharm for code using `napari`. Since around 2 weeks if I try to run a script in debug mode in PyCharm and the script calls one of the `add_xxx` functions, for instance `add_points()`, I receive a `TypeError` exception.

Running the script in Run mode works. Also the debugger works everywhere else. I am using the latest released napari. 

The backtrace for the error I get is the following:
```
/Users/macbook/miniconda3/envs/ome/bin/python /Users/macbook/Applications/PyCharm Professional Edition.app/Contents/plugins/python/helpers/pydev/pydevd.py --multiprocess --qt-support=auto --client 127.0.0.1 --port 58587 --file /Users/macbook/embl/projects/basel/spatialdata/napari-spatialdata/temp/bug_points_scaling.py 
Connected to pydev debugger (build 231.9225.15)
Traceback (most recent call last):
  File "/Users/macbook/Applications/PyCharm Professional Edition.app/Contents/plugins/python/helpers/pydev/pydevd.py", line 1496, in _exec
    pydev_imports.execfile(file, globals, locals)  # execute the script
  File "/Users/macbook/Applications/PyCharm Professional Edition.app/Contents/plugins/python/helpers/pydev/_pydev_imps/_pydev_execfile.py", line 18, in execfile
    exec(compile(contents+"\n", file, 'exec'), glob, loc)
  File "/Users/macbook/embl/projects/basel/spatialdata/napari-spatialdata/temp/bug_points_scaling.py", line 4, in <module>
    viewer.add_points([[10, 10]], size=[10], name="points_layer")
  File "/Users/macbook/miniconda3/envs/ome/lib/python3.10/site-packages/napari/components/viewer_model.py", line 7, in add_points
    from functools import lru_cache
TypeError: PostInit.__call__() got multiple values for argument 'self'
python-BaseExceptio
```

### 💡 Steps to Reproduce

1. in PyCharm run the following script in Debug Mode
```python
import napari

viewer = napari.Viewer()
viewer.add_points([[10, 10]], size=[10], name="points_layer")
napari.run()
```

### 💡 Expected Behavior

The script should execute normally.

### 🌎 Environment

napari: 0.4.19.post1
Platform: macOS-13.4.1-arm64-arm-64bit
System: MacOS 13.4.1
Python: 3.10.13 | packaged by conda-forge | (main, Oct 26 2023, 18:09:17) [Clang 16.0.6 ]
Qt: 5.15.8
PyQt5: 5.15.9
NumPy: 1.26.3
SciPy: 1.11.4
Dask: 2023.12.1
VisPy: 0.14.1
magicgui: 0.8.1
superqt: 0.6.1
in-n-out: 0.1.9
app-model: 0.2.4
npe2: 0.7.3

OpenGL:
  - GL version:  2.1 Metal - 83.1
  - MAX_TEXTURE_SIZE: 16384

Screens:
  - screen 1: resolution 3840x2160, scale 1.0
  - screen 2: resolution 3840x2160, scale 1.0

Settings path:
  - /Users/macbook/Library/Application Support/napari/ome_e544a94aa34edcbdceb29c82a6079cdcab084ea5/settings.yaml
Plugins:
  - napari: 0.4.19.post1 (77 contributions)
  - napari-console: 0.0.9 (0 contributions)
  - napari-matplotlib: 1.2.0 (10 contributions)
  - napari-spatialdata: 0.3.2.dev10+g3c8dfd5 (6 contributions)
  - napari-svg: 0.1.10 (2 contributions)

### 💡 Additional Context

_No response_


Discussion:

Hi @LucaMarconato. It looks really strange as self is popped form kwargs here:
https://github.com/napari/napari/blob/daf72055ca729f951b92478ed5e05823492f4433/napari/utils/_register.py#L9

And a line referred by this error is https://github.com/napari/napari/blob/daf72055ca729f951b92478ed5e05823492f4433/napari/utils/_register.py#L13 (we need to fix this generation). 

As I cannot reproduce this on my machine so I could only ask you to try to use debugger with steep by steep and see where is a problem, why `self` is not popped. With such information we could try to debug this. 
Thanks for the answer. I tried using the "Step Over" option in the PyCharm debugger. After 2 steps I get the error:
![CleanShot 2024-03-29 at 12 59 41](https://github.com/napari/napari/assets/2664412/174c8720-7020-49cb-8499-d7606f6ece46)

I tried adding a `print(kwargs)` before and after the `kwargs.pop('self', None)` and the `self` is removed correctly; still I am keeping getting the error.
I haven't tried updating/reinstalling PyCharm or installing `napari` from main. I could try.
Reinstalling napari from main didn't help, working on a fresh conda env neither. I ended up completely removing conda from my machine and reinstalling it, which worked. So now I am finally not experiencing this bug anymore.
I celebrated too early, the problem was not fixed, but now I managed to implement a workaround. 

In details:
- the problem actually still occurred, I just mistakenly didn't call `add_xxxx` when testing the new conda installation
- I switched to `main` (note, I had to do a full install and not an editable install otherwise I was getting a different error upon opening napari)
- Now that https://github.com/napari/napari/pull/6791 is merged, I could see a debugger at `add_layer.py_tmpl` when calling `add_xxxx()`; I verified and `kwargs` contained `self` even after `pop()` was called.
- I modified `add_layer.py_tmpl` to match the code below, and this workaround now the problem disappeared for me. I am reopening the issue.

```
def {name}{signature}:
    kwargs = locals()
    kwargs.pop('self', None)
    pos_kwargs = dict()
    for name in getattr({cls_name}.__init__, "_deprecated_constructor_args", []):
        pos_kwargs[name] = kwargs.pop(name, None)
    kwargs_workaround = dict()
    for key in kwargs.copy():
        if key not in ['self', 'kwargs_workaround', 'kwargs', 'pos_kwargs']:
            kwargs_workaround[key] = kwargs[key]
    layer = {cls_name}(**kwargs_workaround)
    for name, value in pos_kwargs.items():
        if value is not None:
            setattr(layer, name, value)
    self.layers.append(layer)
    return layer

# This file is created to improve user bug reports when
# user use `viewer.add_xxxx` and some error occurs.
# this allow to have more readable stacktrace.
```


What happens if you change `kwargs = locals()` to `kwargs = dict(locals())` without your workaround? 
@LucaMarconato ?
Good point! Yes, now it works, by applying the change you proposed in `napari.utils._register.py`.
@LucaMarconato would you like to open a PR? 😃 

Resolving patch:
diff --git a/napari/utils/_register.py b/napari/utils/_register.py
--- a/napari/utils/_register.py
+++ b/napari/utils/_register.py
@@ -2,6 +2,7 @@
 import sys
 from inspect import Parameter, getdoc, signature
 
+from napari.utils.migrations import rename_argument
 from napari.utils.misc import camel_to_snake
 from napari.utils.translations import trans
 
@@ -74,6 +75,7 @@ def create_func(cls, name=None, doc=None):
         ],
         return_annotation=cls,
     )
+
     src = template.format(
         name=name,
         signature=new_sig,
@@ -94,4 +96,15 @@ def create_func(cls, name=None, doc=None):
         return_annotation=cls,
     )
 
+    if hasattr(cls.__init__, '_rename_argument'):
+        for (
+            from_name,
+            to_name,
+            version,
+            since_version,
+        ) in cls.__init__._rename_argument:
+            func = rename_argument(from_name, to_name, version, since_version)(
+                func
+            )
+
     return func
diff --git a/napari/utils/migrations.py b/napari/utils/migrations.py
--- a/napari/utils/migrations.py
+++ b/napari/utils/migrations.py
@@ -39,6 +39,13 @@ def rename_argument(
         )
 
     def _wrapper(func):
+        if not hasattr(func, '_rename_argument'):
+            func._rename_argument = []
+
+        func._rename_argument.append(
+            (from_name, to_name, version, since_version)
+        )
+
         @wraps(func)
         def _update_from_dict(*args, **kwargs):
             if from_name in kwargs:


Test patch:
diff --git a/napari/utils/_tests/test_register.py b/napari/utils/_tests/test_register.py
--- a/napari/utils/_tests/test_register.py
+++ b/napari/utils/_tests/test_register.py
@@ -1,7 +1,12 @@
 from dataclasses import dataclass, field
 
+import pytest
+
 from napari.utils._register import create_func
-from napari.utils.migrations import deprecated_constructor_arg_by_attr
+from napari.utils.migrations import (
+    deprecated_constructor_arg_by_attr,
+    rename_argument,
+)
 
 
 @dataclass
@@ -22,17 +27,46 @@ class SimpleClassDeprecated:
     """Simple class to test create_func"""
 
     @deprecated_constructor_arg_by_attr('b')
+    @deprecated_constructor_arg_by_attr('c')
     def __init__(self, a=1):
         self.a = a
 
     @property
-    def b(self):
+    def b(self):  # pragma: no cover
         return self.a * 2
 
     @b.setter
     def b(self, value):
         self.a = value // 2
 
+    @property
+    def c(self):  # pragma: no cover
+        return self.a * 2
+
+    @c.setter
+    def c(self, value):
+        self.a = value // 2
+
+
+class SimpleClassRenamed:
+    """Simple class to test create_func"""
+
+    @rename_argument(
+        from_name='c',
+        to_name='a',
+        version='0.6.0',
+        since_version='0.4.18',
+    )
+    @rename_argument(
+        from_name='d',
+        to_name='b',
+        version='0.6.0',
+        since_version='0.4.18',
+    )
+    def __init__(self, a=1, b=1):
+        self.a = a
+        self.b = b
+
 
 def test_create_func():
     DummyClass.add_simple_class = create_func(SimpleClass)
@@ -46,3 +80,16 @@ def test_create_func_deprecated():
     dc = DummyClass()
     dc.add_simple_class_deprecated(b=4)
     assert dc.layers[0].a == 2
+    dc.add_simple_class_deprecated(c=8)
+    assert dc.layers[1].a == 4
+
+
+def test_create_func_renamed():
+    DummyClass.add_simple_class_renamed = create_func(SimpleClassRenamed)
+    dc = DummyClass()
+    with pytest.warns(DeprecationWarning, match="Argument 'c' is deprecated"):
+        dc.add_simple_class_renamed(c=4)
+    assert dc.layers[0].a == 4
+    with pytest.warns(DeprecationWarning, match="Argument 'd' is deprecated"):
+        dc.add_simple_class_renamed(d=8)
+    assert dc.layers[1].b == 8