Skip to content

Commit

Permalink
DEPR: deprecate passing redshift (z) as a keyword argument (astropy…
Browse files Browse the repository at this point in the history
…#16597)

* DEPR: deprecate passing redshift (`z`) as a keyword argument

Signed-off-by: nstarman <nstarman@users.noreply.github.com>
Co-authored-by: nstarman <nstarman@users.noreply.github.com>
  • Loading branch information
neutrinoceros and nstarman committed Jul 10, 2024
1 parent 0168eb1 commit 00f2bba
Show file tree
Hide file tree
Showing 13 changed files with 817 additions and 128 deletions.
244 changes: 244 additions & 0 deletions astropy/cosmology/_signature_deprecations.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
// This extension is adapted from the positional_defaults PyPI package
// https://pypi.org/project/positional-defaults/ version 2023.4.19
// MIT. see licenses/POSITIONAL_DEFAULTS.rst


#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <structmember.h>

#define SINCE_CHAR_SIZE 32
#define NAMES_CHAR_SIZE 128
#define MSG_SIZE 512

typedef struct {
PyObject_HEAD
PyObject* dict;
PyObject* wrapped;
PyObject* names;
PyObject* since;
} DeprKwsObject;



static void
depr_kws_wrap_dealloc(DeprKwsObject* self)
{
Py_XDECREF(self->wrapped);
Py_XDECREF(self->names);
Py_XDECREF(self->since);
Py_TYPE(self)->tp_free((PyObject*)self);
}


static PyObject*
depr_kws_wrap_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
DeprKwsObject* self = (DeprKwsObject*)type->tp_alloc(type, 0);

if (self != NULL) {
self->names = PyTuple_New(0);
if (self->names == NULL) {
Py_DECREF(self);
return NULL;
}

Py_INCREF(Py_None);
self->wrapped = Py_None;

Py_INCREF(Py_None);
self->since = Py_None;
}

return (PyObject*)self;
}


static int
depr_kws_wrap_init(DeprKwsObject* self, PyObject* args, PyObject* kwds)
{
static char *kwlist[] = {"wrapped", "names", "since", NULL};
Py_ssize_t i, n_names;
PyObject* wrapped, *names, *since, *tmp;

if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOO:wrap", kwlist,
&wrapped, &names, &since))
return -1;

if (!PyTuple_Check(names)) {
PyErr_SetString(PyExc_TypeError, "names must be a tuple");
return -1;
}

n_names = PyTuple_GET_SIZE(names);

for (i = 0; i < n_names; ++i) {
PyObject* name = PyTuple_GET_ITEM(names, i);
if (!PyUnicode_Check(name)) {
PyErr_Format(PyExc_TypeError, "names[%zd] must be a string", i);
return -1;
}
}

if (!PyUnicode_Check(since)) {
PyErr_Format(PyExc_TypeError, "since must be a string", i);
return -1;
}

tmp = self->wrapped;
Py_INCREF(wrapped);
self->wrapped = wrapped;
Py_XDECREF(tmp);

tmp = self->names;
Py_INCREF(names);
self->names = names;
Py_XDECREF(tmp);

tmp = self->since;
Py_INCREF(since);
self->since = since;
Py_XDECREF(tmp);
return 0;
}


static PyMemberDef depr_kws_wrap_members[] = {
{"__dict__", T_OBJECT, offsetof(DeprKwsObject, dict), READONLY},
{"wrapped", T_OBJECT, offsetof(DeprKwsObject, wrapped), READONLY},
{"names", T_OBJECT, offsetof(DeprKwsObject, names), READONLY},
{"since", T_OBJECT, offsetof(DeprKwsObject, since), READONLY},
{NULL}
};


static PyObject*
depr_kws_wrap_call(DeprKwsObject* self, PyObject* args, PyObject* kwds) {
// step 0: return early whenever possible
if (self->wrapped == NULL)
Py_RETURN_NONE;

if (kwds == NULL)
return PyObject_Call(self->wrapped, args, kwds);

// step 1: detect any deprecated keyword arguments, return if none.
Py_ssize_t n_names = PyTuple_GET_SIZE(self->names);
PyObject *deprecated_kwargs = PyList_New(n_names);
Py_INCREF(deprecated_kwargs);
PyObject *name = NULL;
Py_ssize_t i = 0;
int has_kw = -2;

Py_ssize_t n_depr = 0;
for (i=0 ; i < n_names ; ++i) {
name = PyTuple_GET_ITEM(self->names, i);
has_kw = PyDict_Contains(kwds, name);
if (has_kw) {
PyList_SET_ITEM(deprecated_kwargs, n_depr, name);
++n_depr;
}
}

if (n_depr == 0)
return PyObject_Call(self->wrapped, args, kwds);

// step 2: create and emit warning message
char names_char[NAMES_CHAR_SIZE];
char *s, *arguments, *respectively, *pronoun;

PyObject *names_unicode;
if (n_depr > 1) {
names_unicode = PyObject_Str(PyList_GetSlice(deprecated_kwargs, 0, n_depr));
s = "s";
arguments = " arguments";
respectively = ", respectively";
pronoun = "them";
} else {
names_unicode = PyObject_Repr(PyList_GET_ITEM(deprecated_kwargs, 0));
s = arguments = respectively = "";
pronoun = "it";
}
const char* names_utf8 = PyUnicode_AsUTF8(names_unicode);
snprintf(names_char, NAMES_CHAR_SIZE, "%s", names_utf8);

PyObject *since_unicode = PyObject_Str(self->since);
const char* since_utf8 = PyUnicode_AsUTF8(since_unicode);
char since_char[SINCE_CHAR_SIZE];
snprintf(since_char, SINCE_CHAR_SIZE, "%s", since_utf8);

char msg[MSG_SIZE];
snprintf(
msg,
MSG_SIZE,
"Passing %s%s as keyword%s "
"is deprecated since version %s "
"and will stop working in a future release. "
"Pass %s positionally to suppress this warning.",
names_char, arguments, s, since_char, pronoun
);
const char* msg_ptr = msg;

int status = PyErr_WarnEx(PyExc_FutureWarning, msg_ptr, 2);
if (status == -1) {
// avoid leaking memory if Warning is promoted to Exception
Py_DECREF(deprecated_kwargs);
}

return PyObject_Call(self->wrapped, args, kwds);
}


static PyObject*
depr_kws_wrap_get(PyObject* self, PyObject* obj, PyObject* type) {
if (obj == Py_None || obj == NULL) {
Py_INCREF(self);
return self;
}
return PyMethod_New(self, obj);
}


static PyTypeObject DeprKwsWrap = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "_signature_deprecations.wrap",
.tp_doc = PyDoc_STR("wrap a function with deprecated keyword arguments"),
.tp_basicsize = sizeof(DeprKwsObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_dictoffset = offsetof(DeprKwsObject, dict),
.tp_new = depr_kws_wrap_new,
.tp_init = (initproc)depr_kws_wrap_init,
.tp_dealloc = (destructor)depr_kws_wrap_dealloc,
.tp_members = depr_kws_wrap_members,
.tp_call = (ternaryfunc)depr_kws_wrap_call,
.tp_descr_get = depr_kws_wrap_get,
};


static struct PyModuleDef module = {
PyModuleDef_HEAD_INIT,
.m_name = "_signature_deprecations",
.m_doc = PyDoc_STR("fast decorators to mark signature details as deprecated"),
.m_size = -1,
};


PyMODINIT_FUNC
PyInit__signature_deprecations(void) {
PyObject* m;

if (PyType_Ready(&DeprKwsWrap) < 0)
return NULL;

m = PyModule_Create(&module);
if (m == NULL)
return NULL;

Py_INCREF(&DeprKwsWrap);
if (PyModule_AddObject(m, "_depr_kws_wrap", (PyObject*)&DeprKwsWrap) < 0) {
Py_DECREF(&DeprKwsWrap);
Py_DECREF(m);
return NULL;
}

return m;
}
28 changes: 25 additions & 3 deletions astropy/cosmology/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,23 @@

import functools
import operator
from collections.abc import Callable
from dataclasses import Field
from numbers import Number
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, TypeVar

import numpy as np

from astropy.units import Quantity

from . import units as cu
from ._signature_deprecations import _depr_kws_wrap

if TYPE_CHECKING:
from typing import Any

from astropy.cosmology import Parameter

_F = TypeVar("_F", bound=Callable[..., Any])


def vectorize_redshift_method(func=None, nin=1):
"""Vectorize a method of redshift(s).
Expand Down Expand Up @@ -118,3 +120,23 @@ def all_parameters(obj: object, /) -> dict[str, Field | Parameter]:
or (isinstance(v, Field) and isinstance(v.default, Parameter))
)
}


def deprecated_keywords(*kws, since):
"""Deprecate calling one or more arguments as keywords.
Parameters
----------
*kws: str
Names of the arguments that will become positional-only.
since : str or number or sequence of str or number
The release at which the old argument became deprecated.
"""
return functools.partial(_depr_kws, kws=kws, since=since)


def _depr_kws(func: _F, /, kws: tuple[str, ...], since: str) -> _F:
wrapper = _depr_kws_wrap(func, kws, since)
functools.update_wrapper(wrapper, func)
return wrapper
Loading

0 comments on commit 00f2bba

Please sign in to comment.