jaraco / jaraco/keyring

Type checking error due to casting a subclass as `backend.KeyringBackend` base class

Open
#645 3 comments 0 reactions 0 assignees View on GitHub
good-first-issue help wanted
Dominant language
Python
Stars
1.5k
Forks
196
PR merge metrics
No merged PRs in 30d

Description

**Describe the bug**

I am attempting to contribute an implementation using this library to a type checked project and mypy is failing the PR.

I think the cause of the failure is that the backend returned from `keyring.get_keyring()` is typed as the wrong class. It's being cast to `keyring.backend.Backend` but it is in fact a subclass.

```py
>>> import keyring
>>> kr = keyring.get_keyring()
>>> type(kr)

>>> kr = keyring.get_keyring()
>>> type(kr)

>>> type(kr).mro()
[, , , ]
```

https://github.com/jaraco/keyring/blob/af7233958ece36da8d493509a6388484f5534dfc/keyring/core.py#L30-L34

Type checkers reject this incorrect casting whenever a method is used that's unique to a subclass, such as `get_preferred_collection()` which exists on `keyring.backends.SecretService.Keyring` but not `keyring.backend.KeyringBackend`.

```
pydantic_settings/sources.py:666: error: Incompatible types in assignment (expression has type "KeyringBackend", variable has type "Keyring") [assignment]
pydantic_settings/sources.py:676: error: Call to untyped function "get_preferred_collection" in typed context [no-untyped-call]
Found 2 errors in 1 file (checked 5 source files)
```

due to this code at line 666 :imp: :

```py
663 def _read_keyring(self, case_sensitive: bool) -> Mapping[str, str | None]:
664 keyring_backend = self.keyring_backend
665 if keyring_backend is None:
666 kr: SecretServiceKeyring = keyring.core.get_keyring()
667 else:
668 all_keyrings: Iterator[SecretServiceKeyring] = keyring.backend.get_all_keyring()
```

and further on at 676:

```py
675 keyring_vars: dict[str, str | None] = {}
676 kr_collection: SecretServiceCollection = kr.get_preferred_collection()
677 kr_items = kr_collection.get_all_items()
678 keyring_vars.update({item.get_attributes()['service']: item.get_secret().decode() for item in kr_items})
679 return keyring_vars
```

It looks tricky but maybe I can help make some progress on these type annotations, maybe the cast helps identify a place to start?

I can't figure out if there is already a mypy workflow in use in this project (is `pytest-mypy` called somewhere?)

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with keyring/core.py lines 30-34 and the backend definitions to compare the get_keyring() annotation with the concrete SecretService.Keyring usage shown in pydantic_settings/sources.py. Reproduce the reported mypy errors and determine an annotation approach that preserves valid subclass-specific use; done means the affected type checks pass without incorrect casts.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.