diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index 35aad4c897..543b3e6b02 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -14,6 +14,7 @@ from django.db import models from django.http import Http404 from django.template.engine import Engine from django.utils.decorators import method_decorator +from django.utils.inspect import func_has_no_args from django.utils.translation import ugettext as _ from django.views.generic import TemplateView @@ -243,7 +244,7 @@ class ModelDetailView(BaseAdminDocsView): # Gather model methods. for func_name, func in model.__dict__.items(): - if (inspect.isfunction(func) and len(inspect.getargspec(func)[0]) == 1): + if inspect.isfunction(func) and func_has_no_args(func): try: for exclude in MODEL_METHODS_EXCLUDE: if func_name.startswith(exclude): diff --git a/django/contrib/gis/management/commands/ogrinspect.py b/django/contrib/gis/management/commands/ogrinspect.py index 1194cf6cfb..f161c06e89 100644 --- a/django/contrib/gis/management/commands/ogrinspect.py +++ b/django/contrib/gis/management/commands/ogrinspect.py @@ -1,8 +1,8 @@ import argparse -import inspect from django.contrib.gis import gdal from django.core.management.base import BaseCommand, CommandError +from django.utils.inspect import get_func_args class LayerOptionAction(argparse.Action): @@ -91,7 +91,7 @@ class Command(BaseCommand): from django.contrib.gis.utils.ogrinspect import _ogrinspect, mapping # Filter options to params accepted by `_ogrinspect` ogr_options = {k: v for k, v in options.items() - if k in inspect.getargspec(_ogrinspect).args and v is not None} + if k in get_func_args(_ogrinspect) and v is not None} output = [s for s in _ogrinspect(ds, model_name, **ogr_options)] if options['mapping']: diff --git a/django/core/files/storage.py b/django/core/files/storage.py index 65b6d46463..554f05f2a9 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -2,7 +2,6 @@ import errno import os import warnings from datetime import datetime -from inspect import getargspec from django.conf import settings from django.core.exceptions import SuspiciousFileOperation @@ -14,6 +13,7 @@ from django.utils.deconstruct import deconstructible from django.utils.deprecation import RemovedInDjango20Warning from django.utils.encoding import filepath_to_uri, force_text from django.utils.functional import LazyObject +from django.utils.inspect import func_supports_parameter from django.utils.module_loading import import_string from django.utils.six.moves.urllib.parse import urljoin from django.utils.text import get_valid_filename @@ -49,8 +49,7 @@ class Storage(object): if not hasattr(content, 'chunks'): content = File(content) - args, varargs, varkw, defaults = getargspec(self.get_available_name) - if 'max_length' in args: + if func_supports_parameter(self.get_available_name, 'max_length'): name = self.get_available_name(name, max_length=max_length) else: warnings.warn( diff --git a/django/db/migrations/writer.py b/django/db/migrations/writer.py index a0832ff4f6..a7848d744f 100644 --- a/django/db/migrations/writer.py +++ b/django/db/migrations/writer.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import collections import datetime import decimal -import inspect import math import os import re @@ -18,6 +17,7 @@ from django.utils import datetime_safe, six from django.utils._os import upath from django.utils.encoding import force_text from django.utils.functional import Promise +from django.utils.inspect import get_func_args from django.utils.module_loading import module_dir from django.utils.timezone import utc from django.utils.version import get_docs_version @@ -97,7 +97,7 @@ class OperationWriter(object): imports = set() name, args, kwargs = self.operation.deconstruct() - argspec = inspect.getargspec(self.operation.__init__) + operation_args = get_func_args(self.operation.__init__) # See if this operation is in django.db.migrations. If it is, # We can just use the fact we already have that imported, @@ -110,15 +110,14 @@ class OperationWriter(object): self.indent() - # Start at one because argspec includes "self" - for i, arg in enumerate(args, 1): + for i, arg in enumerate(args): arg_value = arg - arg_name = argspec.args[i] + arg_name = operation_args[i] _write(arg_name, arg_value) i = len(args) # Only iterate over remaining arguments - for arg_name in argspec.args[i + 1:]: + for arg_name in operation_args[i:]: if arg_name in kwargs: # Don't sort to maintain signature order arg_value = kwargs[arg_name] _write(arg_name, arg_value) diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index 3f4c7064e4..aaed449fc6 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -1,7 +1,6 @@ import datetime import os import warnings -from inspect import getargspec from django import forms from django.core import checks @@ -13,6 +12,7 @@ from django.db.models.fields import Field from django.utils import six from django.utils.deprecation import RemovedInDjango20Warning from django.utils.encoding import force_str, force_text +from django.utils.inspect import func_supports_parameter from django.utils.translation import ugettext_lazy as _ @@ -89,8 +89,7 @@ class FieldFile(File): def save(self, name, content, save=True): name = self.field.generate_filename(self.instance, name) - args, varargs, varkw, defaults = getargspec(self.storage.save) - if 'max_length' in args: + if func_supports_parameter(self.storage.save, 'max_length'): self.name = self.storage.save(name, content, max_length=self.field.max_length) else: warnings.warn( diff --git a/django/db/utils.py b/django/db/utils.py index 8a27fdc0e2..c6d32a7e9f 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -290,8 +290,16 @@ class ConnectionRouter(object): # If the router doesn't have a method, skip to the next one. continue - argspec = inspect.getargspec(router.allow_migrate) - if len(argspec.args) == 3 and not argspec.keywords: + if six.PY3: + sig = inspect.signature(router.allow_migrate) + has_deprecated_signature = not any( + p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values() + ) + else: + argspec = inspect.getargspec(router.allow_migrate) + has_deprecated_signature = len(argspec.args) == 3 and not argspec.keywords + + if has_deprecated_signature: warnings.warn( "The signature of allow_migrate has changed from " "allow_migrate(self, db, model) to " diff --git a/django/dispatch/dispatcher.py b/django/dispatch/dispatcher.py index a8181b7ec6..b2d774bda6 100644 --- a/django/dispatch/dispatcher.py +++ b/django/dispatch/dispatcher.py @@ -4,6 +4,7 @@ import warnings import weakref from django.utils.deprecation import RemovedInDjango21Warning +from django.utils.inspect import func_accepts_kwargs from django.utils.six.moves import range if sys.version_info < (3, 4): @@ -89,24 +90,11 @@ class Signal(object): # If DEBUG is on, check that we got a good receiver if settings.configured and settings.DEBUG: - import inspect assert callable(receiver), "Signal receivers must be callable." # Check for **kwargs - # Not all callables are inspectable with getargspec, so we'll - # try a couple different ways but in the end fall back on assuming - # it is -- we don't want to prevent registration of valid but weird - # callables. - try: - argspec = inspect.getargspec(receiver) - except TypeError: - try: - argspec = inspect.getargspec(receiver.__call__) - except (TypeError, AttributeError): - argspec = None - if argspec: - assert argspec[2] is not None, \ - "Signal receivers must accept keyword arguments (**kwargs)." + if not func_accepts_kwargs(receiver): + raise ValueError("Signal receivers must accept keyword arguments (**kwargs).") if dispatch_uid: lookup_key = (dispatch_uid, _make_id(sender)) diff --git a/django/template/base.py b/django/template/base.py index deebde7a7c..7f33d79dc7 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -51,10 +51,10 @@ u'' from __future__ import unicode_literals +import inspect import logging import re import warnings -from inspect import getargspec, getcallargs from django.template.context import ( # NOQA: imported for backwards compatibility BaseContext, Context, ContextPopException, RequestContext, @@ -66,6 +66,7 @@ from django.utils.encoding import ( ) from django.utils.formats import localize from django.utils.html import conditional_escape, escape +from django.utils.inspect import getargspec from django.utils.safestring import ( EscapeData, SafeData, mark_for_escaping, mark_safe, ) @@ -723,7 +724,8 @@ class FilterExpression(object): plen = len(provided) + 1 # Check to see if a decorator is providing the real function. func = getattr(func, '_decorated_function', func) - args, varargs, varkw, defaults = getargspec(func) + + args, _, _, defaults = getargspec(func) alen = len(args) dlen = len(defaults or []) # Not enough OR Too many @@ -884,7 +886,7 @@ class Variable(object): current = current() except TypeError: try: - getcallargs(current) + inspect.getcallargs(current) except TypeError: # arguments *were* required current = context.template.engine.string_if_invalid # invalid method call else: diff --git a/django/template/library.py b/django/template/library.py index 182dacf964..6d8a2fb675 100644 --- a/django/template/library.py +++ b/django/template/library.py @@ -1,10 +1,10 @@ import functools import warnings from importlib import import_module -from inspect import getargspec from django.utils import six from django.utils.deprecation import RemovedInDjango21Warning +from django.utils.inspect import getargspec from django.utils.itercompat import is_iterable from .base import Node, Template, token_kwargs diff --git a/django/template/loaders/base.py b/django/template/loaders/base.py index 4c429f7883..dd6b52e2bb 100644 --- a/django/template/loaders/base.py +++ b/django/template/loaders/base.py @@ -1,8 +1,8 @@ import warnings -from inspect import getargspec from django.template import Origin, Template, TemplateDoesNotExist from django.utils.deprecation import RemovedInDjango21Warning +from django.utils.inspect import func_supports_parameter class Loader(object): @@ -28,7 +28,7 @@ class Loader(object): args = [template_name] # RemovedInDjango21Warning: Add template_dirs for compatibility with # old loaders - if 'template_dirs' in getargspec(self.get_template_sources)[0]: + if func_supports_parameter(self.get_template_sources, 'template_dirs'): args.append(template_dirs) for origin in self.get_template_sources(*args): diff --git a/django/template/loaders/cached.py b/django/template/loaders/cached.py index 4c9aa3077b..54146fbfbb 100644 --- a/django/template/loaders/cached.py +++ b/django/template/loaders/cached.py @@ -5,11 +5,11 @@ to load templates from them in order, caching the result. import hashlib import warnings -from inspect import getargspec from django.template import Origin, Template, TemplateDoesNotExist from django.utils.deprecation import RemovedInDjango21Warning from django.utils.encoding import force_bytes +from django.utils.inspect import func_supports_parameter from .base import Loader as BaseLoader @@ -51,7 +51,7 @@ class Loader(BaseLoader): args = [template_name] # RemovedInDjango21Warning: Add template_dirs for compatibility # with old loaders - if 'template_dirs' in getargspec(loader.get_template_sources)[0]: + if func_supports_parameter(loader.get_template_sources, 'template_dirs'): args.append(template_dirs) for origin in loader.get_template_sources(*args): yield origin diff --git a/django/utils/deprecation.py b/django/utils/deprecation.py index a31392abd1..3b8dd1f3a4 100644 --- a/django/utils/deprecation.py +++ b/django/utils/deprecation.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import inspect import warnings diff --git a/django/utils/inspect.py b/django/utils/inspect.py new file mode 100644 index 0000000000..3e3ad0ac23 --- /dev/null +++ b/django/utils/inspect.py @@ -0,0 +1,80 @@ +from __future__ import absolute_import + +import inspect + +from django.utils import six + + +def getargspec(func): + if six.PY2: + return inspect.getargspec(func) + + sig = inspect.signature(func) + args = [ + p.name for p in sig.parameters.values() + if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD + ] + varargs = [ + p.name for p in sig.parameters.values() + if p.kind == inspect.Parameter.VAR_POSITIONAL + ] + varargs = varargs[0] if varargs else None + varkw = [ + p.name for p in sig.parameters.values() + if p.kind == inspect.Parameter.VAR_KEYWORD + ] + varkw = varkw[0] if varkw else None + defaults = [ + p.default for p in sig.parameters.values() + if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD and p.default is not p.empty + ] or None + return args, varargs, varkw, defaults + + +def get_func_args(func): + if six.PY2: + argspec = inspect.getargspec(func) + return argspec.args[1:] # ignore 'self' + + sig = inspect.signature(func) + return [ + arg_name for arg_name, param in sig.parameters.items() + if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD + ] + + +def func_accepts_kwargs(func): + if six.PY2: + # Not all callables are inspectable with getargspec, so we'll + # try a couple different ways but in the end fall back on assuming + # it is -- we don't want to prevent registration of valid but weird + # callables. + try: + argspec = inspect.getargspec(func) + except TypeError: + try: + argspec = inspect.getargspec(func.__call__) + except (TypeError, AttributeError): + argspec = None + return not argspec or argspec[2] is not None + + return any( + p for p in inspect.signature(func).parameters.values() + if p.kind == p.VAR_KEYWORD + ) + + +def func_has_no_args(func): + args = inspect.getargspec(func)[0] if six.PY2 else [ + p for p in inspect.signature(func).parameters.values() + if p.kind == p.POSITIONAL_OR_KEYWORD and p.default is p.empty + ] + return len(args) == 1 + + +def func_supports_parameter(func, parameter): + if six.PY3: + return parameter in inspect.signature(func).parameters + else: + args, varargs, varkw, defaults = inspect.getargspec(func) + return parameter in args