Repository: amitdev/lru-dict (language: python)

Issue:
Add a lru.pyi
add a stub file for better user experience

e.g.
```python
# lru.pyi
from typing import Any, Generic, Hashable, TypeVar, Callable

_K = TypeVar("_K", bound=Hashable)
_V = TypeVar("_V")
_T = TypeVar("_T")

class LRU(Generic[_K, _V]):

    def __init__(self, size: int, callback: Callable[[_K, _V], Any] | None) -> None: ...
    def get(self, key: _K, default: _T) -> _V | _T: ...
    def __getitem__(self, item: _K) -> _V: ...
    def set(self, key: _K, value: Any) -> None: ...
    def __setitem__(self, key: _K, value: Any) -> None: ...
    ...
```


Discussion / hints:
Could you raise a PR for this?

Fix patch:
diff --git a/.gitignore b/.gitignore
index e43b0f9..a056699 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,1 +1,132 @@
-.DS_Store
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pdm config
+.pdm.toml
\ No newline at end of file
diff --git a/MANIFEST.in b/MANIFEST.in
index 5f59047..34da6ea 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,1 +1,4 @@
 include LICENSE README.rst MANIFEST MANIFEST.in
+graft src
+global-exclude *.pyc
+global-exclude *.cache
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..2dc6e95
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,38 @@
+[project]
+name = "lru-dict"
+version = "1.2.0"
+description = "An Dict like LRU container."
+authors = [
+    {name = "Amit Dev"},
+]
+dependencies = []
+requires-python = ">=3.7"
+readme = "README.rst"
+license = {text = "MIT"}
+keywords = ["lru", "dict"]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: MIT License",
+    "Operating System :: OS Independent",
+    "Operating System :: POSIX",
+    "Programming Language :: C",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.7",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: Implementation :: CPython",
+    "Topic :: Software Development :: Libraries :: Python Modules",
+]
+[project.urls]
+Homepage = "https://github.com/amitdev/lru-dict"
+[project.optional-dependencies]
+test = [
+    "pytest",
+]
+
+[build-system]
+requires = ["setuptools>=61", "wheel"]
+build-backend = "setuptools.build_meta"
diff --git a/setup.py b/setup.py
index deca7df..f51b667 100644
--- a/setup.py
+++ b/setup.py
@@ -1,35 +1,12 @@
 from setuptools import setup, Extension
 
-module1 = Extension('lru',
-                    sources = ['lru.c'])
+extensions = [
+    Extension("lru._lru", ["src/lru/_lru.c"]),
+]
 
-setup (name = 'lru-dict',
-       version = '1.2.0',
-       description = 'An Dict like LRU container.',
-       long_description = open('README.rst').read(),
-       long_description_content_type="text/x-rst",
-       author='Amit Dev',
-       url='https://github.com/amitdev/lru-dict',
-       license='MIT',
-       keywords='lru, dict',
-       ext_modules = [module1],
-       classifiers=[
-        'Development Status :: 5 - Production/Stable',
-        'Intended Audience :: Developers',
-        'License :: OSI Approved :: MIT License',
-        'Operating System :: OS Independent',
-        'Operating System :: POSIX',
-        'Programming Language :: C',
-        'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.7',
-        'Programming Language :: Python :: 3.8',
-        'Programming Language :: Python :: 3.9',
-        'Programming Language :: Python :: 3.10',
-        'Programming Language :: Python :: 3.11',
-        'Programming Language :: Python :: Implementation :: CPython',
-        'Topic :: Software Development :: Libraries :: Python Modules',
-        ],
-        extras_require={
-            'test': ['pytest'],
-        },
-)
+args = {
+    "include_package_data": True,
+    "exclude_package_data": {"": ["*.c"]},
+}
+
+setup(ext_modules=extensions, **args)
diff --git a/src/lru/__init__.py b/src/lru/__init__.py
new file mode 100644
index 0000000..2fe1926
--- /dev/null
+++ b/src/lru/__init__.py
@@ -0,0 +1,3 @@
+from ._lru import LRU as LRU  # noqa: F401
+
+__all__ = ["LRU"]
diff --git a/src/lru/__init__.pyi b/src/lru/__init__.pyi
new file mode 100644
index 0000000..d284112
--- /dev/null
+++ b/src/lru/__init__.pyi
@@ -0,0 +1,63 @@
+from typing import (
+    Any,
+    Callable,
+    Generic,
+    Hashable,
+    Iterable,
+    TypeVar,
+    overload,
+    Protocol
+)
+
+_KT = TypeVar("_KT", bound=Hashable)
+_VT = TypeVar("_VT")
+_VT_co = TypeVar("_VT_co", covariant=True)
+_T = TypeVar("_T")
+
+
+class __SupportsKeysAndGetItem(Protocol[_KT, _VT_co]):
+    def keys(self) -> Iterable[_KT]: ...
+    def __getitem__(self, __key: _KT) -> _VT_co: ...
+
+
+class LRU(Generic[_KT, _VT]):
+    @overload
+    def __init__(self, size: int) -> None: ...
+    @overload
+    def __init__(self, size: int, callback: Callable[[_KT, _VT], Any]) -> None: ...
+    def clear(self) -> None: ...
+    @overload
+    def get(self, key: _KT) -> _VT | None: ...
+    @overload
+    def get(self, key: _KT, instead: _VT | _T) -> _VT | _T: ...
+    def get_size(self) -> int: ...
+    def has_key(self, key: _KT) -> bool: ...
+    def keys(self) -> list[_KT]: ...
+    def values(self) -> list[_VT]: ...
+    def items(self) -> list[tuple[_KT, _VT]]: ...
+    def peek_first_item(self) -> tuple[_KT, _VT] | None: ...
+    def peek_last_item(self) -> tuple[_KT, _VT] | None: ...
+    @overload
+    def pop(self, key: _KT) -> _VT | None: ...
+    @overload
+    def pop(self, key: _KT, default: _VT | _T) -> _VT | _T: ...
+    def popitem(self, least_recent: bool = ...) -> tuple[_KT, _VT]: ...
+    @overload
+    def setdefault(self: LRU[_KT, _T | None], key: _KT) -> _T | None: ...
+    @overload
+    def setdefault(self, key: _KT, default: _VT) -> _VT: ...
+    def set_callback(self, callback: Callable[[_KT, _VT], Any] | None) -> None: ...
+    def set_size(self, size: int) -> None: ...
+    @overload
+    def update(self, __m: __SupportsKeysAndGetItem[_KT, _VT], **kwargs: _VT) -> None: ...
+    @overload
+    def update(self, __m: Iterable[tuple[_KT, _VT]], **kwargs: _VT) -> None: ...
+    @overload
+    def update(self, **kwargs: _VT) -> None: ...
+    def get_stats(self) -> tuple[int, int]: ...
+    def __contains__(self, __o: Any) -> bool: ...
+    def __delitem__(self, key: _KT) -> None: ...
+    def __getitem__(self, item: _KT) -> _VT: ...
+    def __len__(self) -> int: ...
+    def __repr__(self) -> str: ...
+    def __setitem__(self, key: _KT, value: _VT) -> None: ...
diff --git a/lru.c b/src/lru/_lru.c
similarity index 99%
rename from lru.c
rename to src/lru/_lru.c
index 33c18ab..8f0f219 100644
--- a/lru.c
+++ b/src/lru/_lru.c
@@ -80,7 +80,7 @@ node_repr(Node* self)
 
 static PyTypeObject NodeType = {
     PyVarObject_HEAD_INIT(NULL, 0)
-    "lru.Node",              /* tp_name */
+    "_lru.Node",              /* tp_name */
     sizeof(Node),            /* tp_basicsize */
     0,                       /* tp_itemsize */
     (destructor)node_dealloc,/* tp_dealloc */
@@ -711,7 +711,7 @@ PyDoc_STRVAR(lru_doc,
 
 static PyTypeObject LRUType = {
     PyVarObject_HEAD_INIT(NULL, 0)
-    "lru.LRU",               /* tp_name */
+    "_lru.LRU",               /* tp_name */
     sizeof(LRU),             /* tp_basicsize */
     0,                       /* tp_itemsize */
     (destructor)LRU_dealloc, /* tp_dealloc */
@@ -753,7 +753,7 @@ static PyTypeObject LRUType = {
 #if PY_MAJOR_VERSION >= 3
   static struct PyModuleDef moduledef = {
     PyModuleDef_HEAD_INIT,
-    "lru",            /* m_name */
+    "_lru",            /* m_name */
     lru_doc,          /* m_doc */
     -1,               /* m_size */
     NULL,             /* m_methods */
@@ -780,7 +780,7 @@ moduleinit(void)
     #if PY_MAJOR_VERSION >= 3
         m = PyModule_Create(&moduledef);
     #else
-        m = Py_InitModule3("lru", NULL, lru_doc);
+        m = Py_InitModule3("_lru", NULL, lru_doc);
     #endif
 
     if (m == NULL)


Test patch:
diff --git a/test/test_lru.py b/test/test_lru.py
index a385795..8a9e16e 100644
--- a/test/test_lru.py
+++ b/test/test_lru.py
@@ -247,11 +247,11 @@ def test_pop(self):
         self.assertEqual(0, len(l))
         with self.assertRaises(KeyError) as ke:
             l.pop(4)
-            self.assertEqual(4, ke.args[0])
+            self.assertEqual(4, ke.args[0])  # type: ignore
         self.assertEqual((2, 2), l.get_stats())
         self.assertEqual(0, len(l))
         with self.assertRaises(TypeError):
-            l.pop()
+            l.pop()  # type: ignore
 
     def test_popitem(self):
         l = LRU(3)
@@ -265,7 +265,7 @@ def test_popitem(self):
         self.assertEqual((2, '2'), l.popitem(True))
         with self.assertRaises(KeyError) as ke:
             l.popitem()
-            self.assertEqual('popitem(): LRU dict is empty', ke.args[0])
+            self.assertEqual('popitem(): LRU dict is empty', ke.args[0])  # type: ignore
         self.assertEqual((0, 0), l.get_stats())
 
     def test_stats(self):