Repository: micheles/decorator

Issue:
decorator whether to support decorator partial functions？
I looked at the code and it wasn't supported. Can we add a judgment here that supports partial functions？
src/decorator.py:74
`f inspect.isroutine(func):`

Discussion:

Solution patch:
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index fda580a..7931125 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -14,20 +14,22 @@ jobs:
 
     runs-on: ubuntu-latest
     strategy:
+      fail-fast: false
       matrix:
-        python-version: ['3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11']
+        python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v4
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
+        allow-prereleases: true
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
         python -m pip install codespell flake8
         python -m pip install -e .
-    - run: codespell --ignore-words-list="ba,claus,vas" --quiet-level=2
+    - run: codespell --ignore-words-list="assertIn,ba,claus,vas" --quiet-level=2
     - run: flake8 . --count --ignore=E122,E226,E265,E741,E742 --max-complexity=22 --max-line-length=124 --show-source --statistics
     - run: python src/tests/test.py -v
diff --git a/CHANGES.md b/CHANGES.md
index f76724b..6188471 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -3,6 +3,8 @@ HISTORY
 
 ## Unreleased
 
+Dropped support for Python < 3.7 and added support for Python 3.11 and 3.12.
+
 ## 5.1.1 (2022-01-07)
 
 Sangwoo Shim contributed a fix so that cythonized functions can be decorated.
diff --git a/LICENSE.txt b/LICENSE.txt
index b0ade04..eb5a679 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -5,9 +5,10 @@ Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are
 met:
 
-  Redistributions of source code must retain the above copyright
+* Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
-  Redistributions in bytecode form must reproduce the above copyright
+
+* Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in
   the documentation and/or other materials provided with the
   distribution.
diff --git a/docs/documentation.md b/docs/documentation.md
index cc92eb9..4757f37 100644
--- a/docs/documentation.md
+++ b/docs/documentation.md
@@ -4,7 +4,7 @@
 |---|---|
 |E-mail | michele.simionato@gmail.com|
 |Version| 5.1.1 (2022-01-07)|
-|Supports| Python 3.5, 3.6, 3.7, 3.8, 3.9, 3.10, 3.11|
+|Supports| Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13|
 |Download page| https://pypi.org/project/decorator/5.1.1|
 |Installation| ``pip install decorator``|
 |License | BSD license|
@@ -1692,4 +1692,4 @@ DAMAGE.
 
 If you use this software and you are happy with it, consider sending me a
 note, just to gratify my ego. On the other hand, if you use this software and
-you are unhappy with it, send me a patch!
\ No newline at end of file
+you are unhappy with it, send me a patch!
diff --git a/setup.py b/setup.py
index d71c24b..46b7c21 100644
--- a/setup.py
+++ b/setup.py
@@ -13,25 +13,25 @@ if __name__ == '__main__':
           author='Michele Simionato',
           author_email='michele.simionato@gmail.com',
           url='https://github.com/micheles/decorator',
-          license="new BSD License",
+          license="BSD-2-Clause",
           package_dir={'': 'src'},
           py_modules=['decorator'],
           keywords="decorators generic utility",
           platforms=["All"],
-          python_requires='>=3.5',
+          python_requires='>=3.7',
           classifiers=['Development Status :: 5 - Production/Stable',
                        'Intended Audience :: Developers',
                        'License :: OSI Approved :: BSD License',
                        'Natural Language :: English',
                        'Operating System :: OS Independent',
                        'Programming Language :: Python',
-                       'Programming Language :: Python :: 3.5',
-                       'Programming Language :: Python :: 3.6',
                        '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 :: 3.12',
+                       'Programming Language :: Python :: 3.13',
                        'Programming Language :: Python :: Implementation :: CPython',
                        'Topic :: Software Development :: Libraries',
                        'Topic :: Utilities'],
diff --git a/src/decorator.py b/src/decorator.py
index 2479b6f..35e5b18 100644
--- a/src/decorator.py
+++ b/src/decorator.py
@@ -37,6 +37,7 @@ import sys
 import inspect
 import operator
 import itertools
+import functools
 from contextlib import _GeneratorContextManager
 from inspect import getfullargspec, iscoroutinefunction, isgeneratorfunction
 
@@ -71,7 +72,7 @@ class FunctionMaker(object):
                 self.name = '_lambda_'
             self.doc = func.__doc__
             self.module = func.__module__
-            if inspect.isroutine(func):
+            if inspect.isroutine(func) or isinstance(func, functools.partial):
                 argspec = getfullargspec(func)
                 self.annotations = getattr(func, '__annotations__', {})
                 for a in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs',
@@ -214,6 +215,8 @@ def decorate(func, caller, extras=(), kwsyntax=False):
     does. By default kwsyntax is False and the the arguments are untouched.
     """
     sig = inspect.signature(func)
+    if isinstance(func, functools.partial):
+        func = functools.update_wrapper(func, func.func)
     if iscoroutinefunction(caller):
         async def fun(*args, **kw):
             if not kwsyntax:
@@ -230,6 +233,7 @@ def decorate(func, caller, extras=(), kwsyntax=False):
             if not kwsyntax:
                 args, kw = fix(args, kw, sig)
             return caller(func, *(extras + args), **kw)
+
     fun.__name__ = func.__name__
     fun.__doc__ = func.__doc__
     fun.__wrapped__ = func


Test patch:
diff --git a/src/tests/documentation.py b/src/tests/documentation.py
index bbbe562..6fb80f1 100644
--- a/src/tests/documentation.py
+++ b/src/tests/documentation.py
@@ -14,7 +14,7 @@ doc = r"""# Decorators for Humans
 |---|---|
 |E-mail | michele.simionato@gmail.com|
 |Version| $VERSION ($DATE)|
-|Supports| Python 3.5, 3.6, 3.7, 3.8, 3.9, 3.10, 3.11|
+|Supports| Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12|
 |Download page| https://pypi.org/project/decorator/$VERSION|
 |Installation| ``pip install decorator``|
 |License | BSD license|
diff --git a/src/tests/test.py b/src/tests/test.py
index be9f851..6172be4 100644
--- a/src/tests/test.py
+++ b/src/tests/test.py
@@ -3,6 +3,7 @@ import doctest
 import unittest
 import decimal
 import inspect
+import functools
 from asyncio import get_event_loop
 from collections import defaultdict, ChainMap, abc as c
 from decorator import dispatch_on, contextmanager, decorator
@@ -509,5 +510,20 @@ class TestSingleDispatch(unittest.TestCase):
             h(u)
 
 
+@decorator
+def partial_before_after(func, *args, **kwargs):
+    return "<before>" + func(*args, **kwargs) + "<after>"
+
+
+class PartialTestCase(unittest.TestCase):
+    def test_before_after(self):
+        def origin_func(x, y):
+            return x + y
+        _func = functools.partial(origin_func, "x")
+        partial_func = partial_before_after(_func)
+        out = partial_func("y")
+        self.assertEqual(out, '<before>xy<after>')
+
+
 if __name__ == '__main__':
     unittest.main()