Skip to content

Commit

Permalink
Python 3.13 Support (#1226)
Browse files Browse the repository at this point in the history
* Add Python 3.13 tests to tox

* Fix memcache tests

* Fix agent unittests under developer mode

* Add wheels for 3.13

* Replace PyEval_CallObject with PyObject_Call in C extensions

* Remove Python 2 specific code for C extensions

* Update includes for C extensions for Py 3.13 compatibility

* Add support for setuptools_scm v7 and v8

* Address feedback

* Adding additional tox envs that are now supported
  • Loading branch information
TimPansino authored Oct 8, 2024
1 parent 1807857 commit 3a001b0
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 127 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/deploy-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:
- cp311-musllinux
- cp312-manylinux
- cp312-musllinux
- cp313-manylinux
- cp313-musllinux

steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # 4.1.1
Expand All @@ -48,7 +50,7 @@ jobs:
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # 3.0.0

- name: Build Wheels
uses: pypa/cibuildwheel@8d945475ac4b1aac4ae08b2fd27db9917158b6ce # 2.17.0
uses: pypa/cibuildwheel@d4a2945fcc8d13f20a1b99d461b8e844d5fc6e23 # 2.21.1
env:
CIBW_PLATFORM: linux
CIBW_BUILD: "${{ matrix.wheel }}*"
Expand Down
13 changes: 0 additions & 13 deletions newrelic/common/_monotonic.c
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ static PyMethodDef monotonic_methods[] = {
{ NULL, NULL }
};

#if PY_MAJOR_VERSION >= 3
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"_monotonic", /* m_name */
Expand All @@ -133,36 +132,24 @@ static struct PyModuleDef moduledef = {
NULL, /* m_clear */
NULL, /* m_free */
};
#endif

static PyObject *
moduleinit(void)
{
PyObject *module;

#if PY_MAJOR_VERSION >= 3
module = PyModule_Create(&moduledef);
#else
module = Py_InitModule3("_monotonic", monotonic_methods, NULL);
#endif

if (module == NULL)
return NULL;

return module;
}

#if PY_MAJOR_VERSION < 3
PyMODINIT_FUNC init_monotonic(void)
{
moduleinit();
}
#else
PyMODINIT_FUNC PyInit__monotonic(void)
{
return moduleinit();
}
#endif

/* ------------------------------------------------------------------------- */

21 changes: 2 additions & 19 deletions newrelic/core/_thread_utilization.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

/* ------------------------------------------------------------------------- */

#include <sys/time.h>
#include <Python.h>

#include <pythread.h>

#ifndef PyVarObject_HEAD_INIT
Expand Down Expand Up @@ -254,14 +254,10 @@ static PyObject *NRUtilization_enter(NRUtilizationObject *self, PyObject *args)
PyObject *func = NULL;

dict = PyModule_GetDict(module);
#if PY_MAJOR_VERSION >= 3
func = PyDict_GetItemString(dict, "current_thread");
#else
func = PyDict_GetItemString(dict, "currentThread");
#endif
if (func) {
Py_INCREF(func);
thread = PyEval_CallObject(func, (PyObject *)NULL);
thread = PyObject_Call(func, (PyObject *)NULL, (PyObject *)NULL);
if (!thread)
PyErr_Clear();

Expand Down Expand Up @@ -408,7 +404,6 @@ PyTypeObject NRUtilization_Type = {

/* ------------------------------------------------------------------------- */

#if PY_MAJOR_VERSION >= 3
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"_thread_utilization", /* m_name */
Expand All @@ -420,18 +415,13 @@ static struct PyModuleDef moduledef = {
NULL, /* m_clear */
NULL, /* m_free */
};
#endif

static PyObject *
moduleinit(void)
{
PyObject *module;

#if PY_MAJOR_VERSION >= 3
module = PyModule_Create(&moduledef);
#else
module = Py_InitModule3("_thread_utilization", NULL, NULL);
#endif

if (module == NULL)
return NULL;
Expand All @@ -446,16 +436,9 @@ moduleinit(void)
return module;
}

#if PY_MAJOR_VERSION < 3
PyMODINIT_FUNC init_thread_utilization(void)
{
moduleinit();
}
#else
PyMODINIT_FUNC PyInit__thread_utilization(void)
{
return moduleinit();
}
#endif

/* ------------------------------------------------------------------------- */
6 changes: 5 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@


def newrelic_agent_guess_next_version(tag_version):
if hasattr(tag_version, "tag"): # For setuptools_scm 7.0+
tag_version = tag_version.tag

version, _, _ = str(tag_version).partition("+")
version_info = list(map(int, version.split(".")))
if len(version_info) < 3:
Expand Down Expand Up @@ -142,6 +145,7 @@ def build_extension(self, ext):
"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",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: System :: Monitoring",
Expand All @@ -155,7 +159,7 @@ def build_extension(self, ext):
"git_describe_command": "git describe --dirty --tags --long --match *.*.*",
"write_to": "newrelic/version.txt",
},
setup_requires=["setuptools_scm>=3.2,<7"],
setup_requires=["setuptools_scm>=3.2,<9"],
description="New Relic Python Agent",
long_description=open(readme_file).read(),
url="https://docs.newrelic.com/docs/apm/agents/python-agent/",
Expand Down
15 changes: 6 additions & 9 deletions tests/agent_unittests/test_full_uri_payloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
from testing_support.fixtures import collector_agent_registration_fixture
from newrelic.core.agent_protocol import AgentProtocol
from newrelic.common.agent_http import HttpClient
from newrelic.core.config import global_settings
from newrelic.core.config import global_settings, _environ_as_bool

DEVELOPER_MODE = _environ_as_bool("NEW_RELIC_DEVELOPER_MODE", False) or "NEW_RELIC_LICENSE_KEY" not in os.environ
SKIP_IF_DEVELOPER_MODE = pytest.mark.skipif(DEVELOPER_MODE, reason="Cannot connect to collector in developer mode")


class FullUriClient(HttpClient):
Expand Down Expand Up @@ -55,10 +58,7 @@ def session(application):
}


@pytest.mark.skipif(
"NEW_RELIC_LICENSE_KEY" not in os.environ,
reason="License key is not expected to be valid",
)
@SKIP_IF_DEVELOPER_MODE
@pytest.mark.parametrize(
"method,payload",
[
Expand Down Expand Up @@ -86,10 +86,7 @@ def test_full_uri_payload(session, method, payload):
protocol.send(method, payload)


@pytest.mark.skipif(
"NEW_RELIC_LICENSE_KEY" not in os.environ,
reason="License key is not expected to be valid",
)
@SKIP_IF_DEVELOPER_MODE
def test_full_uri_connect():
# An exception will be raised here if there's a problem with the response
AgentProtocol.connect(
Expand Down
4 changes: 3 additions & 1 deletion tests/datastore_aiomcache/test_aiomcache.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@

from newrelic.api.background_task import background_task
from newrelic.api.transaction import set_background_task
from newrelic.common import system_info

DB_SETTINGS = memcached_settings()[0]

MEMCACHED_HOST = DB_SETTINGS["host"]
MEMCACHED_PORT = DB_SETTINGS["port"]
MEMCACHED_NAMESPACE = str(os.getpid())
INSTANCE_METRIC_NAME = f"Datastore/instance/Memcached/{MEMCACHED_HOST}/{MEMCACHED_PORT}"
INSTANCE_METRIC_HOST = system_info.gethostname() if MEMCACHED_HOST == "127.0.0.1" else MEMCACHED_HOST
INSTANCE_METRIC_NAME = f"Datastore/instance/Memcached/{INSTANCE_METRIC_HOST}/{MEMCACHED_PORT}"

_test_bt_set_get_delete_scoped_metrics = [
("Datastore/operation/Memcached/set", 1),
Expand Down
4 changes: 3 additions & 1 deletion tests/datastore_bmemcached/test_memcache.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@

from newrelic.api.background_task import background_task
from newrelic.api.transaction import set_background_task
from newrelic.common import system_info

DB_SETTINGS = memcached_settings()[0]

MEMCACHED_HOST = DB_SETTINGS["host"]
MEMCACHED_PORT = DB_SETTINGS["port"]
MEMCACHED_NAMESPACE = str(os.getpid())
MEMCACHED_ADDR = f"{MEMCACHED_HOST}:{MEMCACHED_PORT}"
INSTANCE_METRIC_NAME = f"Datastore/instance/Memcached/{MEMCACHED_HOST}/{MEMCACHED_PORT}"
INSTANCE_METRIC_HOST = system_info.gethostname() if MEMCACHED_HOST == "127.0.0.1" else MEMCACHED_HOST
INSTANCE_METRIC_NAME = f"Datastore/instance/Memcached/{INSTANCE_METRIC_HOST}/{MEMCACHED_PORT}"

_test_bt_set_get_delete_scoped_metrics = [
("Datastore/operation/Memcached/set", 1),
Expand Down
10 changes: 7 additions & 3 deletions tests/datastore_pymemcache/test_memcache.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@
from newrelic.api.background_task import background_task
from newrelic.api.transaction import set_background_task

from newrelic.common import system_info

DB_SETTINGS = memcached_settings()[0]

MEMCACHED_HOST = DB_SETTINGS["host"]
MEMCACHED_PORT = DB_SETTINGS["port"]
MEMCACHED_NAMESPACE = DB_SETTINGS["namespace"]

MEMCACHED_ADDR = (MEMCACHED_HOST, int(MEMCACHED_PORT))
INSTANCE_METRIC_HOST = system_info.gethostname() if MEMCACHED_HOST == "127.0.0.1" else MEMCACHED_HOST
INSTANCE_METRIC_NAME = f"Datastore/instance/Memcached/{INSTANCE_METRIC_HOST}/{MEMCACHED_PORT}"


_test_bt_set_get_delete_scoped_metrics = [
("Datastore/operation/Memcached/set", 1),
Expand All @@ -43,7 +47,7 @@
("Datastore/operation/Memcached/set", 1),
("Datastore/operation/Memcached/get", 1),
("Datastore/operation/Memcached/delete", 1),
(f"Datastore/instance/Memcached/{MEMCACHED_HOST}/{MEMCACHED_PORT}", 3),
(INSTANCE_METRIC_NAME, 3),
]


Expand Down Expand Up @@ -81,7 +85,7 @@ def test_bt_set_get_delete():
("Datastore/operation/Memcached/set", 1),
("Datastore/operation/Memcached/get", 1),
("Datastore/operation/Memcached/delete", 1),
(f"Datastore/instance/Memcached/{MEMCACHED_HOST}/{MEMCACHED_PORT}", 3),
(INSTANCE_METRIC_NAME, 3),
]


Expand Down
Loading

0 comments on commit 3a001b0

Please sign in to comment.