Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ jobs:

strategy:
matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "pypy3"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "pypy3"]

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: "actions/checkout@v2"
- uses: "actions/setup-python@v2"
- uses: "actions/checkout@v6"
- uses: "actions/setup-python@v6"
with:
python-version: "${{ matrix.python-version }}"
- name: "Install dependencies"
Expand All @@ -34,15 +34,16 @@ jobs:
python -VV
python -m site
python -m pip install --upgrade pip setuptools wheel
python -m pip install --upgrade virtualenv tox tox-gh-actions
# FIXME: Make tox.ini compatible with newer versions of tox
python -m pip install --upgrade virtualenv 'tox<4' tox-gh-actions
- name: "Run tox targets for ${{ matrix.python-version }}"
run: "python -m tox"

- name: "Report to coveralls"
# coverage is only created in the py39 environment
# coverage is only created in the py314 environment
# --service=github is a workaround for bug
# https://github.com/coveralls-clients/coveralls-python/issues/251
if: "matrix.python-version == '3.9'"
if: "matrix.python-version == '3.14'"
run: |
pip install coveralls
coveralls --service=github
Expand Down
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
repos:
- repo: https://github.com/psf/black
rev: 20.8b1
rev: 26.3.1
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: "3.8.4"
rev: "7.3.0"
hooks:
- id: flake8
- repo: https://github.com/asottile/pyupgrade
rev: v2.7.4
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py36-plus]
14 changes: 13 additions & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@ CHANGES
0.13 (unreleased)
=================

- Drop support for Python below 3.6
- ``reg.arginfo`` is now implemented manually using
``inspect.signature``, instead of relying the deprecated
compatibility method in ``inspect``, in order to better
support Python 3.14+.

It is recommended to migrate from ``reg.arginfo`` to pure
``inspect.signature``, since the utility this module provides
is now very limited, compared to ``inspect.signature``, since
that already properly handles ``self`` parameters.

- Added support for Python 3.10, 3.11, 3.12, 3.13 and 3.14

- Drop support for Python below 3.10

- Use GitHub Actions for CI

Expand Down
3 changes: 2 additions & 1 deletion develop_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# development
-e '.[test,coverage,pep8,docs]'
pre-commit
tox >= 2.4.1
# FIXME: Make tox.ini compatible with newer versions of tox
tox >= 2.4.1, < 4
radon

# releaser
Expand Down
135 changes: 79 additions & 56 deletions reg/arginfo.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
import inspect
import sys

if sys.version_info < (3, 14):

def get_signature(callable):
"""A compatibility wrapper for `inspect.signature`."""
return inspect.signature(callable)

else:
from annotationlib import Format

def get_signature(callable):
"""A compatibility wrapper for `inspect.signature`."""
return inspect.signature(callable, annotation_format=Format.FORWARDREF)


def arginfo(callable):
Expand All @@ -10,7 +24,10 @@ def arginfo(callable):
:func:`inspect.getfullargspec` returns information about the arguments
of a function. arginfo also works for classes and instances with a
__call__ defined. Unlike getfullargspec, arginfo treats bound methods
like functions, so that the self argument is not reported.
like functions, so that the self argument is not reported. Another
difference is the handling of decorated functions. This will return
the original signature, rather than the signature of the wrapper, if
wrapped via :func:`functools.wraps`.

arginfo returns ``None`` if given something that is not callable.

Expand All @@ -29,22 +46,67 @@ def arginfo(callable):
return arginfo._cache[callable.__call__]
except (AttributeError, KeyError):
pass
func, cache_key, remove_self = get_callable_info(callable)
if func is None:
return None
result = inspect.getfullargspec(func)
if remove_self:
args = result.args[1:]
result = inspect.FullArgSpec(
args,
result.varargs,
result.varkw,
result.defaults,
result.kwonlyargs,
result.kwonlydefaults,
result.annotations,
)
arginfo._cache[cache_key] = result

if inspect.isfunction(callable):
cache_key = callable
elif inspect.ismethod(callable):
cache_key = callable
elif inspect.isclass(callable):
cache_key = callable
if callable.__init__ is WRAPPER_DESCRIPTOR:
# Only in this specific case do we replace the callable
# into `inspect.signature` with something else, to ensure
# we don't get a `ValueError` and instead end up with
# an empty signature.
callable = fake_empty_init
else:
# Since arbitrary callable objects may not be hashable
# we instead retrieve their call method, which should be
try:
cache_key = callable.__call__
except AttributeError:
return None

signature = get_signature(callable)
args = []
varargs = None
varkw = None
defaults = []
kwonlyargs = []
kwonlydefaults = {}
annotations = {}
for parameter in signature.parameters.values():
if (
parameter.kind is parameter.POSITIONAL_OR_KEYWORD
or parameter.kind is parameter.POSITIONAL_ONLY
):
args.append(parameter.name)
if parameter.default is not parameter.empty:
defaults.append(parameter.default)
elif parameter.kind is parameter.KEYWORD_ONLY:
kwonlyargs.append(parameter.name)
if parameter.default is not parameter.empty:
kwonlydefaults[parameter.name] = parameter.default
elif parameter.kind is parameter.VAR_POSITIONAL:
varargs = parameter.name
elif parameter.kind is parameter.VAR_KEYWORD:
varkw = parameter.name

if parameter.annotation is not parameter.empty:
annotations[parameter.name] = parameter.annotation

if signature.return_annotation is not signature.empty:
annotations["return"] = signature.return_annotation

result = arginfo._cache[cache_key] = inspect.FullArgSpec(
args,
varargs,
varkw,
tuple(defaults) if defaults else None,
kwonlyargs,
kwonlydefaults,
annotations,
)
return result


Expand All @@ -58,35 +120,6 @@ def is_cached(callable):
arginfo.is_cached = is_cached


def get_callable_info(callable):
"""Get information about a callable.

Returns a tuple of:

* actual function/method that can be inspected with inspect.getfullargspec.

* cache key to use to cache results.

* whether to remove self or not.

Note that in Python 3, __init__ is not a method, but we still
want to remove self from it.

If not inspectable (None, None, False) is returned.
"""
if inspect.isfunction(callable):
return callable, callable, False
if inspect.ismethod(callable):
return callable, callable, True
if inspect.isclass(callable):
return get_class_init(callable), callable, True
try:
callable = getattr(callable, "__call__")
return callable, callable, True
except AttributeError:
return None, None, False


def fake_empty_init():
pass # pragma: nocoverage

Expand All @@ -96,13 +129,3 @@ class Dummy:


WRAPPER_DESCRIPTOR = Dummy.__init__


def get_class_init(class_):
func = class_.__init__

# If this is a new-style class and there is no __init__
# defined this is a WRAPPER_DESCRIPTOR.
if func is WRAPPER_DESCRIPTOR:
return fake_empty_init
return func
23 changes: 8 additions & 15 deletions reg/tests/test_docgen.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pydoc
import sys
from sphinx.application import Sphinx
from .fixtures.module import Foo, foo

Expand Down Expand Up @@ -31,42 +32,37 @@ class Foo({builtins}.object)
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
| dictionary for instance variables{postamble}
|
| __weakref__
| list of weak references to the object (if defined)
| list of weak references to the object{postamble}
""".format(
builtins=object.__module__
builtins=object.__module__,
postamble=" (if defined)" if sys.version_info < (3, 11) else "",
)
)


def test_dispatch_method_help(capsys):
pydoc.help(Foo.bar)
out, err = capsys.readouterr()
assert (
rstrip_lines(out)
== """\
assert rstrip_lines(out) == """\
Help on function bar in module reg.tests.fixtures.module:

bar(self, obj)
Return the bar of an object.
"""
)


def test_dispatch_help(capsys):
pydoc.help(foo)
out, err = capsys.readouterr()
assert (
rstrip_lines(out)
== """\
assert rstrip_lines(out) == """\
Help on function foo in module reg.tests.fixtures.module:

foo(obj)
return the foo of an object.
"""
)


def test_autodoc(tmpdir):
Expand All @@ -80,9 +76,7 @@ def test_autodoc(tmpdir):
# remove it.
app = Sphinx(root, root, root + "/build", root, "text", status=None)
app.build()
assert (
tmpdir.join("build/contents.txt").read()
== """\
assert tmpdir.join("build/contents.txt").read() == """\
Sample module for testing autodoc.

class reg.tests.fixtures.module.Foo
Expand All @@ -101,4 +95,3 @@ class reg.tests.fixtures.module.Foo

return the foo of an object.
"""
)
9 changes: 5 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Topic :: Software Development :: Libraries :: Python Modules",
"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 :: 3.14",
"Programming Language :: Python :: Implementation :: PyPy",
"Development Status :: 5 - Production/Stable",
],
Expand Down
12 changes: 6 additions & 6 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
[tox]
envlist = py36, py37, py38, py39, pypy3, coverage, pre-commit, docs, perf
envlist = py310, py311, py312, py313, py314, pypy3, coverage, pre-commit, docs, perf
skipsdist = True
skip_missing_interpreters = True

[testenv]
usedevelop = True
extras = test

commands = pytest {posargs}

[testenv:coverage]
Expand Down Expand Up @@ -34,10 +33,11 @@ commands = python {toxinidir}/tox_perf.py

[gh-actions]
python =
3.6: py36
3.7: py37, perf
3.8: py38
3.9: py39, pre-commit, mypy, coverage
3.10: py310, perf
3.11: py311
3.12: py312
3.13: py313
3.14: py314, pre-commit, coverage

[flake8]
max-line-length = 88
Expand Down