changeset 189:6ff66311cfe5

Provide a uwsgi language plugin for PyPy2 also
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 15 Mar 2025 12:20:38 +0100
parents e18b5861868b
children 556b1c7ce0ce
files uwsginl-plugin-lang-pypy2/Makefile uwsginl-plugin-lang-pypy2/distinfo uwsginl-plugin-lang-pypy2/files/extra/patch-plugins_pypy_pypy__plugin.c uwsginl-plugin-lang-pypy2/files/extra/patch-plugins_pypy_pypy__setup.py uwsginl-plugin-lang-pypy2/misc/make-extra-patches.sh uwsginl-plugin-lang-pypy2/pkg-descr
diffstat 6 files changed, 461 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/uwsginl-plugin-lang-pypy2/Makefile	Sat Mar 15 12:20:38 2025 +0100
@@ -0,0 +1,75 @@
+# Created by: Franz Glasner <freebsd-dev@dom66.de>
+
+PORTNAME=	${UWSGI_NAME}-plugin-lang
+PKGNAMESUFFIX=	-${FLAVOR}
+#DISTVERSION=	2.0.20
+PORTVERSION=	2.1.pl9.g${MYGH_TAG_DATE}
+CATEGORIES=	www python
+# Use GitHub id for now (before official 2.1); see below
+#MASTER_SITES=	https://projects.unbit.it/downloads/
+#DISTNAME=	uwsgi-${DISTVERSION}
+
+MAINTAINER=	freebsd-dev@dom66.de
+COMMENT=	Language plugin for PyPy 2
+WWW=		https://projects.unbit.it/uwsgi/
+
+LICENSE=	GPLv2-WITH-LINKING-EXCEPTION
+LICENSE_GROUPS=	FSF GPL OSI
+LICENSE_NAME=	GPLv2 with linking exception
+LICENSE_FILE=	${WRKSRC}/LICENSE
+LICENSE_PERMS=	dist-mirror dist-sell pkg-mirror pkg-sell auto-accept
+
+FLAVORS=	pypy2
+FLAVOR?=	pypy2
+
+BUILD_DEPENDS+=	${UWSGI_NAME}==${PORTVERSION}:www/uwsginl
+BUILD_DEPENDS+=	${PYPY_PACKAGE}>=7:lang/${PYPY_PACKAGE}
+RUN_DEPENDS+=	${UWSGI_NAME}==${PORTVERSION}:www/uwsginl
+RUN_DEPENDS+=	${PYPY_PACKAGE}>=7:lang/${PYPY_PACKAGE}
+
+USES=		cpe gettext-runtime pkgconfig
+CPE_VENDOR=	unbit
+
+.include        "${.CURDIR}/../uwsginl/Makefile.gh"
+
+EXTRA_PATCHES=	${.CURDIR}/files/extra
+
+PLIST_FILES=	${PLUGIN_DIR}/${PLUGIN_FILENAME}
+
+.if ${FLAVOR} == pypy2
+PLUGIN_NAME=	pypy2
+PYPY_PACKAGE=	pypy2
+PYPY_EXECUTABLE=	pypy2.7
+PYPY_LDLIBRARY=	libpypy-c.so
+PYPY_LIBDIR=	lib-python/2.7
+.endif
+
+CFLAGS+=	-DPYPY_LDLIBRARY="${PYPY_LDLIBRARY}" -DPYPY_LIBDIR="${PYPY_LIBDIR}" -DPYPY_PLUGIN_NAME="${PLUGIN_NAME}"
+
+MAKE_ENV+=	UWSGI_PROFILE_OVERRIDE="plugin_build_dir=${STAGEDIR}${PREFIX}/${PLUGIN_DIR};plugin_dir=${PREFIX}/${PLUGIN_DIR}" PYTHON=${LOCALBASE}/bin/pypy3
+
+DESCR=		pkg-descr
+
+UWSGI_NAME=	uwsginl
+UWSGI_PATH=	${LOCALBASE}/bin/${UWSGI_NAME}
+PLUGIN_DIR=	lib/${UWSGI_NAME}/plugins
+
+# Where to find the sources for the plugin (defaults to plugins/${PLUGIN_NAME})
+PLUGIN_SOURCE=	plugins/pypy
+# The complete basename of the plugin
+PLUGIN_FILENAME=	${PLUGIN_NAME}_plugin.so
+
+# Use the PATCHDIR of the binary executable by default
+PATCHDIR?=		${.CURDIR}/../uwsginl/files
+
+do-configure:
+	@${DO_NADA}
+
+do-build:
+	@${MKDIR} ${STAGEDIR}${PREFIX}/${PLUGIN_DIR}
+	@(cd ${BUILD_WRKSRC}; ${SETENV} ${MAKE_ENV} ${UWSGI_PATH} --build-plugin "${PLUGIN_SOURCE} ${PLUGIN_NAME}")
+
+do-install:
+	${INSTALL_LIB} ${BUILD_WRKSRC}/${PLUGIN_FILENAME} ${STAGEDIR}${PREFIX}/${PLUGIN_DIR}
+
+.include <bsd.port.mk>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/uwsginl-plugin-lang-pypy2/distinfo	Sat Mar 15 12:20:38 2025 +0100
@@ -0,0 +1,3 @@
+TIMESTAMP = 1741948883
+SHA256 (unbit-uwsgi-2.1.pl9.g20241026-89cb161cda959697a4afe013f348b06646b960aa_GH0.tar.gz) = 957c6962655c3fdf81128ed2e39c42dd6eb1c3e426357e40594394e63c8935c0
+SIZE (unbit-uwsgi-2.1.pl9.g20241026-89cb161cda959697a4afe013f348b06646b960aa_GH0.tar.gz) = 866454
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/uwsginl-plugin-lang-pypy2/files/extra/patch-plugins_pypy_pypy__plugin.c	Sat Mar 15 12:20:38 2025 +0100
@@ -0,0 +1,166 @@
+--- plugins/pypy/pypy_plugin.c.orig	2024-10-26 11:39:02.000000000 +0200
++++ plugins/pypy/pypy_plugin.c	2025-03-15 12:00:42.355380000 +0100
+@@ -9,6 +9,24 @@
+ 
+ #include <uwsgi.h>
+ 
++#if !defined(PYPY_PLUGIN_NAME)
++#error PYPY_PLUGIN_NAME not defined
++#endif
++#if !defined(PYPY_LDLIBRARY)
++#error PYPY_LDLIBRARY not defined
++#endif
++#if !defined(PYPY_LIBDIR)
++#error PYPY_LIBDIR not defined
++#endif
++
++#define STR2(__x) #__x
++
++#define STR(__x) STR2(__x)
++
++#define CONCAT2(__x, __y) __x##__y
++
++#define CONCAT(__x, __y) CONCAT2(__x, __y)
++
+ struct uwsgi_pypy {
+ 	void *handler;
+ 	char *lib;
+@@ -42,9 +60,10 @@ void (*uwsgi_pypy_post_fork_hook)(void);
+ void (*uwsgi_pypy_hook_pythonpath)(char *);
+ void (*uwsgi_pypy_hook_request)(void *, int);
+ void (*uwsgi_pypy_post_fork_hook)(void);
++void (*uwsgi_pypy_hook_atexit)(void);
+ 
+ extern struct uwsgi_server uwsgi;
+-struct uwsgi_plugin pypy_plugin;
++struct uwsgi_plugin CONCAT(PYPY_PLUGIN_NAME, _plugin);
+ 
+ static int uwsgi_pypy_init() {
+ 
+@@ -58,7 +77,7 @@ static int uwsgi_pypy_init() {
+ 	}
+ 
+ 	if (dlsym(RTLD_DEFAULT, "rpython_startup_code")) {
+-		uwsgi_log("PyPy runtime detected, skipping libpypy-c loading\n");
++		uwsgi_log("PyPy runtime detected, skipping " STR(PYPY_LDLIBRARY) " loading\n");
+ 		goto ready;
+ 	}
+ 	else if (upypy.lib) {
+@@ -68,26 +87,26 @@ static int uwsgi_pypy_init() {
+ 		if (upypy.home) {
+ 			// first try with /bin way:
+ #ifdef __CYGWIN__
+-                        char *libpath = uwsgi_concat2(upypy.home, "/bin/libpypy-c.dll");
++			char *libpath = uwsgi_concat2(upypy.home, "/bin/" STR(PYPY_LDLIBRARY));
+ #elif defined(__APPLE__)
+-                        char *libpath = uwsgi_concat2(upypy.home, "/bin/libpypy-c.dylib");
++			char *libpath = uwsgi_concat2(upypy.home, "/bin/" STR(PYPY_LDLIBRARY));
+ #else
+-                        char *libpath = uwsgi_concat2(upypy.home, "/bin/libpypy-c.so");
++			char *libpath = uwsgi_concat2(upypy.home, "/bin/" STR(PYPY_LDLIBRARY));
+ #endif
+ 			if (uwsgi_file_exists(libpath)) {
+-                                upypy.handler = dlopen(libpath, RTLD_NOW | RTLD_GLOBAL);
+-                        }
+-                        free(libpath);
++				upypy.handler = dlopen(libpath, RTLD_NOW | RTLD_GLOBAL);
++			}
++			free(libpath);
+ 
+ 			// fallback to old-style way
+ 			if (!upypy.handler) {
+ 			
+ #ifdef __CYGWIN__
+-                        	char *libpath = uwsgi_concat2(upypy.home, "/libpypy-c.dll");
++				char *libpath = uwsgi_concat2(upypy.home, "/" STR(PYPY_LDLIBRARY));
+ #elif defined(__APPLE__)
+-                        	char *libpath = uwsgi_concat2(upypy.home, "/libpypy-c.dylib");
++				char *libpath = uwsgi_concat2(upypy.home, "/" STR(PYPY_LDLIBRARY));
+ #else
+-                        	char *libpath = uwsgi_concat2(upypy.home, "/libpypy-c.so");
++				char *libpath = uwsgi_concat2(upypy.home, "/" STR(PYPY_LDLIBRARY));
+ #endif
+ 				if (uwsgi_file_exists(libpath)) {
+ 					upypy.handler = dlopen(libpath, RTLD_NOW | RTLD_GLOBAL);
+@@ -98,11 +117,11 @@ static int uwsgi_pypy_init() {
+ 		// fallback to standard library search path
+ 		if (!upypy.handler) {
+ #ifdef __CYGWIN__
+-			upypy.handler = dlopen("libpypy-c.dll", RTLD_NOW | RTLD_GLOBAL);
++			upypy.handler = dlopen(STR(PYPY_LDLIBRARY), RTLD_NOW | RTLD_GLOBAL);
+ #elif defined(__APPLE__)
+-			upypy.handler = dlopen("libpypy-c.dylib", RTLD_NOW | RTLD_GLOBAL);
++			upypy.handler = dlopen(STR(PYPY_LDLIBRARY), RTLD_NOW | RTLD_GLOBAL);
+ #else
+-			upypy.handler = dlopen("libpypy-c.so", RTLD_NOW | RTLD_GLOBAL);
++			upypy.handler = dlopen(STR(PYPY_LDLIBRARY), RTLD_NOW | RTLD_GLOBAL);
+ #endif
+ 		}
+ 	}
+@@ -126,7 +145,7 @@ static int uwsgi_pypy_init() {
+ 
+ 	u_pypy_init_threads = dlsym(upypy.handler, "pypy_init_threads");
+         if (!u_pypy_init_threads) {
+-                uwsgi_log("!!! WARNING your libpypy-c does not export pypy_init_threads, multithreading will not work !!!\n");
++                uwsgi_log("!!! WARNING your " STR(PYPY_LDLIBRARY) " does not export pypy_init_threads, multithreading will not work !!!\n");
+         }
+ 	
+ 	u_rpython_startup_code();
+@@ -140,7 +159,7 @@ static int uwsgi_pypy_init() {
+ 	}
+ 
+ 	if (u_pypy_setup_home(upypy.home, 0)) {
+-		char *retry = uwsgi_concat2(upypy.home, "/lib_pypy");
++		char *retry = uwsgi_concat2(upypy.home, "/" STR(PYPY_LIBDIR));
+ 		if (uwsgi_is_dir(retry)) {
+ 			// this time we use debug
+ 			if (!u_pypy_setup_home(retry, 1)) {
+@@ -161,7 +180,7 @@ ready:
+ 
+ 	u_pypy_thread_attach = dlsym(upypy.handler, "pypy_thread_attach");
+         if (!u_pypy_thread_attach) {
+-                uwsgi_log("!!! WARNING your libpypy-c does not export pypy_thread_attach, multithreading will not work !!!\n");
++                uwsgi_log("!!! WARNING your " STR(PYPY_LDLIBRARY) " does not export pypy_thread_attach, multithreading will not work !!!\n");
+         }
+ 
+ 	if (upypy.setup) {
+@@ -266,13 +285,17 @@ static void uwsgi_pypy_init_apps() {
+ 	}
+ }
+ 
+-/*
++
+ static void uwsgi_pypy_atexit() {
+-	if (pypy_debug_file)
+-		fflush(pypy_debug_file);
++	/* NOTE: this function is NOT called when "skip-atexit = true" is configured */
++	if (!uwsgi_pypy_hook_atexit) {
++		uwsgi_log("!!! Your pypy setup does not define a uwsgi_pypy_atexit !!!\n");	       
++		return;
++	}	     
++	uwsgi_pypy_hook_atexit();
+ }
+-*/
+ 
++
+ static void uwsgi_opt_pypy_ini_paste(char *opt, char *value, void *foobar) {
+         uwsgi_opt_load_ini(opt, value, NULL);
+         upypy.paste = value;
+@@ -372,8 +395,8 @@ static int uwsgi_pypy_mule(char *opt) {
+ }
+ 
+ 
+-struct uwsgi_plugin pypy_plugin = {
+-	.name = "pypy",
++struct uwsgi_plugin CONCAT(PYPY_PLUGIN_NAME, _plugin) = {
++	.name = STR(PYPY_PLUGIN_NAME),
+ 	.modifier1 = 0,
+ 	.on_load = uwsgi_pypy_onload,
+ 	.init = uwsgi_pypy_init,
+@@ -388,4 +411,6 @@ struct uwsgi_plugin pypy_plugin = {
+ 	.rpc = uwsgi_pypy_rpc,
+ 	.post_fork = uwsgi_pypy_post_fork,
+ 	.mule = uwsgi_pypy_mule,
++
++	.atexit = uwsgi_pypy_atexit,
+ };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/uwsginl-plugin-lang-pypy2/files/extra/patch-plugins_pypy_pypy__setup.py	Sat Mar 15 12:20:38 2025 +0100
@@ -0,0 +1,209 @@
+--- plugins/pypy/pypy_setup.py.orig	2024-10-26 11:39:02.000000000 +0200
++++ plugins/pypy/pypy_setup.py	2025-03-15 12:13:38.474221000 +0100
+@@ -31,6 +31,7 @@
+ extern void (*uwsgi_pypy_hook_pythonpath)(char *);
+ extern void (*uwsgi_pypy_hook_request)(struct wsgi_request *);
+ extern void (*uwsgi_pypy_post_fork_hook)(void);
++extern void (*uwsgi_pypy_hook_atexit)(void);
+ '''
+ 
+ # here we load CFLAGS and uwsgi.h from the binary
+@@ -48,8 +49,8 @@
+ uwsgi_defines = []
+ uwsgi_cflags = ffi.string(lib0.uwsgi_get_cflags()).split()
+ for cflag in uwsgi_cflags:
+-    if cflag.startswith(b'-D'):
+-        line = cflag[2:].decode()
++    if cflag.startswith('-D'):
++        line = cflag[2:]
+         if '=' in line:
+             (key, value) = line.split('=', 1)
+             uwsgi_cdef.append('#define %s ...' % key)
+@@ -59,6 +60,12 @@
+             uwsgi_defines.append('#define %s 1' % line)
+ uwsgi_dot_h = ffi.string(lib0.uwsgi_get_dot_h())
+ 
++#
++# Replace #include <pcre.h> on FreeBSD because it is found on a non-standard
++# location for cffi.
++#
++uwsgi_dot_h = uwsgi_dot_h.replace('#include <pcre.h>', '#include "/usr/local/include/pcre.h"')
++
+ # uwsgi definitions
+ cdefines = '''
+ %s
+@@ -110,6 +117,8 @@
+         uint64_t running_time;
+         uint64_t avg_response_time;
+         uint64_t tx;
++
++        int hijacked;
+         ...;
+ };
+ 
+@@ -164,11 +173,14 @@
+         struct wsgi_request *wsgi_req;
+ 
+         struct uwsgi_plugin *p[];
++
++        int skip_atexit_teardown;
++
+         ...;
+ };
+ extern struct uwsgi_server uwsgi;
+ 
+-extern struct uwsgi_plugin pypy_plugin;
++extern struct uwsgi_plugin pypy2_plugin;
+ 
+ extern const char *uwsgi_pypy_version;
+ 
+@@ -269,9 +281,9 @@
+ %s
+ 
+ extern struct uwsgi_server uwsgi;
+-extern struct uwsgi_plugin pypy_plugin;
++extern struct uwsgi_plugin pypy2_plugin;
+ %s
+-''' % ('\n'.join(uwsgi_defines), uwsgi_dot_h.decode(), hooks)
++''' % ('\n'.join(uwsgi_defines), uwsgi_dot_h, hooks)
+ 
+ ffi.cdef(cdefines)
+ lib = ffi.verify(cverify)
+@@ -286,7 +298,7 @@
+ 
+ # fix argv if needed
+ if len(sys.argv) == 0:
+-    sys.argv.insert(0, ffi.string(lib.uwsgi_binary_path()).decode())
++    sys.argv.insert(0, ffi.string(lib.uwsgi_binary_path()))
+ 
+ 
+ @ffi.callback("void(char *)")
+@@ -305,7 +317,7 @@
+     load a wsgi module
+     """
+     global wsgi_application
+-    m = ffi.string(module).decode()
++    m = ffi.string(module)
+     c = 'application'
+     if ':' in m:
+         m, c = m.split(':')
+@@ -324,7 +336,7 @@
+     global wsgi_application
+     w = ffi.string(filename)
+     c = 'application'
+-    mod = imp.load_source('uwsgi_file_wsgi', w.decode())
++    mod = imp.load_source('uwsgi_file_wsgi', w)
+     wsgi_application = getattr(mod, c)
+ 
+ 
+@@ -334,7 +346,7 @@
+     load a .ini paste app
+     """
+     global wsgi_application
+-    c = ffi.string(config).decode()
++    c = ffi.string(config)
+     if c.startswith('config:'):
+         c = c[7:]
+     if c[0] != '/':
+@@ -363,11 +375,46 @@
+     """
+     add an item to the pythonpath
+     """
+-    path = ffi.string(item).decode()
++    path = ffi.string(item)
+     sys.path.append(path)
+     print("added %s to pythonpath" % path)
+ 
+ 
++@ffi.callback("void()")
++def uwsgi_pypy_atexit():
++    """
++    .atexit handler implementation
++
++    Modelled after python_plugin.c
++    """
++    mywid = lib.uwsgi.mywid
++    if mywid > 0:
++        # if hijacked do not run atexit hooks
++        if lib.uwsgi.workers[mywid].hijacked:
++            return
++        # if busy do not run atexit hooks
++        if lib.uwsgi_worker_is_busy(mywid):
++            return
++        # managing atexit in async mode is a real pain...skip it for now
++        if getattr(lib.uwsgi, "async") > 0:
++            return
++
++    import uwsgi
++    uahandler = getattr(uwsgi, "atexit", None)
++    if callable(uahandler):
++        uahandler()
++
++    if lib.uwsgi.skip_atexit_teardown:
++        return
++
++    import atexit
++    aefn = getattr(atexit, "_run_exitfuncs", None)
++    if callable(aefn):
++        aefn()
++    else:
++        print("!!! atexit._run_exitfuncs() not found !!!")
++
++
+ class WSGIfilewrapper(object):
+     """
+     class implementing wsgi.file_wrapper
+@@ -470,17 +517,15 @@
+     def start_response(status, headers, exc_info=None):
+         if exc_info:
+             traceback.print_exception(*exc_info)
+-        status = status.encode()
+         lib.uwsgi_response_prepare_headers(wsgi_req, ffi.new("char[]", status), len(status))
+         for hh in headers:
+-            hh = (hh[0].encode(), hh[1].encode())
+             lib.uwsgi_response_add_header(wsgi_req, ffi.new("char[]", hh[0]), len(hh[0]), ffi.new("char[]", hh[1]), len(hh[1]))
+         return writer
+ 
+     environ = {}
+     iov = wsgi_req.hvec
+     for i in range(0, wsgi_req.var_cnt, 2):
+-        environ[ffi.string(ffi.cast("char*", iov[i].iov_base), iov[i].iov_len).decode()] = ffi.string(ffi.cast("char*", iov[i+1].iov_base), iov[i+1].iov_len).decode()
++        environ[ffi.string(ffi.cast("char*", iov[i].iov_base), iov[i].iov_len)] = ffi.string(ffi.cast("char*", iov[i+1].iov_base), iov[i+1].iov_len)
+ 
+     environ['wsgi.version'] = (1, 0)
+     scheme = 'http'
+@@ -525,6 +570,7 @@
+ lib.uwsgi_pypy_hook_pythonpath = uwsgi_pypy_pythonpath
+ lib.uwsgi_pypy_hook_request = uwsgi_pypy_wsgi_handler
+ lib.uwsgi_pypy_post_fork_hook = uwsgi_pypy_post_fork_hook
++lib.uwsgi_pypy_hook_atexit = uwsgi_pypy_atexit
+ 
+ """
+ Here we define the "uwsgi" virtual module
+@@ -539,7 +585,7 @@
+ def uwsgi_pypy_uwsgi_register_signal(signum, kind, handler):
+     cb = ffi.callback('void(int)', handler)
+     uwsgi_gc.append(cb)
+-    if lib.uwsgi_register_signal(signum, ffi.new("char[]", kind), cb, lib.pypy_plugin.modifier1) < 0:
++    if lib.uwsgi_register_signal(signum, ffi.new("char[]", kind), cb, lib.pypy2_plugin.modifier1) < 0:
+         raise Exception("unable to register signal %d" % signum)
+ uwsgi.register_signal = uwsgi_pypy_uwsgi_register_signal
+ 
+@@ -564,7 +610,7 @@
+     rpc_func = uwsgi_pypy_RPC(func)
+     cb = ffi.callback("int(int, char*[], int[], char**)", rpc_func)
+     uwsgi_gc.append(cb)
+-    if lib.uwsgi_register_rpc(ffi.new("char[]", name), ffi.addressof(lib.pypy_plugin), argc, cb) < 0:
++    if lib.uwsgi_register_rpc(ffi.new("char[]", name), ffi.addressof(lib.pypy2_plugin), argc, cb) < 0:
+         raise Exception("unable to register rpc func %s" % name)
+ uwsgi.register_rpc = uwsgi_pypy_uwsgi_register_rpc
+ 
+@@ -1069,7 +1115,7 @@
+ 
+ 
+ def uwsgi_pypy_setup_continulets():
+-    if lib.uwsgi["async"] < 1:
++    if getattr(lib.uwsgi, "async") < 1:
+         raise Exception("pypy continulets require async mode !!!")
+     lib.uwsgi.schedule_to_main = uwsgi_pypy_continulet_switch
+     lib.uwsgi.schedule_to_req = uwsgi_pypy_continulet_schedule
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/uwsginl-plugin-lang-pypy2/misc/make-extra-patches.sh	Sat Mar 15 12:20:38 2025 +0100
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+diff -u --show-c-function plugins/pypy/pypy_plugin.c.orig plugins/pypy/pypy_plugin.c >../../files/extra/patch-plugins_pypy_pypy__plugin.c
+
+diff -u plugins/pypy/pypy_setup.py.orig plugins/pypy/pypy_setup.py >../../files/extra/patch-plugins_pypy_pypy__setup.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/uwsginl-plugin-lang-pypy2/pkg-descr	Sat Mar 15 12:20:38 2025 +0100
@@ -0,0 +1,3 @@
+PyPy2 language plugin for uwsginl.
+Dynamically links to PyPy2's libpypy-c.so.
+Contains a PY2 compatible pypy_setup.py again.