Repository: jaraco/keyring (language: python)

Issue:
devpi-client hook broken
The [``@suppress(KeyringError)``](https://github.com/jaraco/keyring/commit/314c8d36fa06090938f95d2672316471b15a6e65#diff-b0e6ff13ff76becf8aee3446689be4103ae1db3b2ba31748c7cb7b934affc033R17) line breaks the devpi-client hook. It looks like the argument spec is changed by it which causes pluggy to not pass in the arguments correctly. It happens when trying to ``devpi push`` a release to pypi and the password is not set in ``~pypirc`` which causes devpi-client to try the hook.
devpi-client hook broken
The [``@suppress(KeyringError)``](https://github.com/jaraco/keyring/commit/314c8d36fa06090938f95d2672316471b15a6e65#diff-b0e6ff13ff76becf8aee3446689be4103ae1db3b2ba31748c7cb7b934affc033R17) line breaks the devpi-client hook. It looks like the argument spec is changed by it which causes pluggy to not pass in the arguments correctly. It happens when trying to ``devpi push`` a release to pypi and the password is not set in ``~pypirc`` which causes devpi-client to try the hook.


Discussion / hints:
It can more easily be triggered by ``devpi login username``.
I'm slightly surprised that the argument spec isn't present, because [ContextDecorator uses](https://github.com/python/cpython/blob/7e36abbb7815b14777c207dba0fe6fcd41d6d37a/Lib/contextlib.py#L77-L82) [functools.wraps](https://docs.python.org/3/library/functools.html#functools.wraps).
It looks like `inspect.getfullargspec` shows the raw args, but `inspect.signature` resolves the wrapping.

```
>>> inspect.getfullargspec(cl.devpiclient_get_password)
FullArgSpec(args=[], varargs='args', varkw='kwds', defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={})
>>> inspect.signature(cl.devpiclient_get_password)
<Signature (url, username)>
```
[Docs say](https://docs.python.org/3/library/inspect.html#inspect.getfullargspec) that `getfullargspec` is discouraged and `signature` should be used:

> Note that [signature()](https://docs.python.org/3/library/inspect.html#inspect.signature) and [Signature Object](https://docs.python.org/3/library/inspect.html#inspect-signature-object) provide the recommended API for callable introspection, and support additional behaviours (like positional-only arguments) that are sometimes encountered in extension module APIs. This function is retained primarily for use in code that needs to maintain compatibility with the Python 2 inspect module API.
Oh, the issue is known (https://github.com/pytest-dev/pluggy/issues/358) and already linked to #582.
It can more easily be triggered by ``devpi login username``.
I'm slightly surprised that the argument spec isn't present, because [ContextDecorator uses](https://github.com/python/cpython/blob/7e36abbb7815b14777c207dba0fe6fcd41d6d37a/Lib/contextlib.py#L77-L82) [functools.wraps](https://docs.python.org/3/library/functools.html#functools.wraps).
It looks like `inspect.getfullargspec` shows the raw args, but `inspect.signature` resolves the wrapping.

```
>>> inspect.getfullargspec(cl.devpiclient_get_password)
FullArgSpec(args=[], varargs='args', varkw='kwds', defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={})
>>> inspect.signature(cl.devpiclient_get_password)
<Signature (url, username)>
```
[Docs say](https://docs.python.org/3/library/inspect.html#inspect.getfullargspec) that `getfullargspec` is discouraged and `signature` should be used:

> Note that [signature()](https://docs.python.org/3/library/inspect.html#inspect.signature) and [Signature Object](https://docs.python.org/3/library/inspect.html#inspect-signature-object) provide the recommended API for callable introspection, and support additional behaviours (like positional-only arguments) that are sometimes encountered in extension module APIs. This function is retained primarily for use in code that needs to maintain compatibility with the Python 2 inspect module API.
Oh, the issue is known (https://github.com/pytest-dev/pluggy/issues/358) and already linked to #582.

Fix patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index cc050eaf..3259ca83 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,8 @@
+v23.9.3
+-------
+
+* #596: Add workaround for devpi_client hook with wrapped implementation.
+
 v23.9.2
 -------
 
diff --git a/keyring/devpi_client.py b/keyring/devpi_client.py
index dfc02452..6a2b1898 100644
--- a/keyring/devpi_client.py
+++ b/keyring/devpi_client.py
@@ -1,19 +1,34 @@
 import contextlib
+import functools
 
-from pluggy import HookimplMarker
+import pluggy
 
-import keyring
-from keyring.errors import KeyringError
+import keyring.errors
 
 
-hookimpl = HookimplMarker("devpiclient")
+hookimpl = pluggy.HookimplMarker("devpiclient")
 
 
 # https://github.com/jaraco/jaraco.context/blob/c3a9b739/jaraco/context.py#L205
 suppress = type('suppress', (contextlib.suppress, contextlib.ContextDecorator), {})
 
 
+def restore_signature(func):
+    # workaround for pytest-dev/pluggy#358
+    @functools.wraps(func)
+    def wrapper(url, username):
+        return func(url, username)
+
+    return wrapper
+
+
 @hookimpl()
-@suppress(KeyringError)
+@restore_signature
+@suppress(keyring.errors.KeyringError)
 def devpiclient_get_password(url, username):
+    """
+    >>> pluggy._hooks.varnames(devpiclient_get_password)
+    (('url', 'username'), ())
+    >>>
+    """
     return keyring.get_password(url, username)


Test patch:
diff --git a/conftest.py b/conftest.py
index ab0021e0..2dce06d4 100644
--- a/conftest.py
+++ b/conftest.py
@@ -19,5 +19,3 @@ def macos_api_ignore():
 
 
 collect_ignore.extend(['keyring/backends/macOS/api.py'] * macos_api_ignore())
-
-collect_ignore.append('keyring/devpi_client.py')