diff --git a/doc/src/sgml/plpython.sgml b/doc/src/sgml/plpython.sgml
index 1921915..ac989a3 100644
--- a/doc/src/sgml/plpython.sgml
+++ b/doc/src/sgml/plpython.sgml
@@ -164,13 +164,6 @@
- See also the
- document What's
- New In Python 3.0 for more information about porting to
- Python 3.
-
-
-
It is not allowed to use PL/Python based on Python 2 and PL/Python
based on Python 3 in the same session, because the symbols in the
dynamic modules would clash, which could result in crashes of the
@@ -179,6 +172,90 @@
a mismatch is detected. It is possible, however, to use both
PL/Python variants in the same database, from separate sessions.
+
+
+ Converting from Python 2 to Python 3
+
+
+ See the
+ document What's
+ New In Python 3.0 for the Python community's information and
+ recommendations about porting to Python 3.
+
+
+
+ PostgreSQL provides some support for helping
+ you to convert existing Python 2 routines to Python 3. In an
+ installation built with Python 3, there is an
+ extension convert_python3 that changes functions
+ and procedures from the plpythonu
+ and plpython2u languages to
+ the plpython3u language. While doing so, it applies
+ the 2to3 tool described in the above document to
+ the body of each such routine.
+
+
+
+ Using convert_python3 can be as simple as:
+
+CREATE EXTENSION convert_python3;
+CALL convert_python3_all();
+
+ This must be done as database superuser. If you wish, you can drop the
+ extension once you're done converting everything.
+
+
+
+ Since convert_python3 is Python 3 code, be careful
+ not to install or run it in a session that has previously executed any
+ Python 2 code. As explained above, that won't work.
+
+
+
+ convert_python3_all has two optional arguments: the
+ name of the conversion tool to use (by default 2to3,
+ but you might for instance need to provide a full path name) and any
+ special command-line options to provide to it. You might for example
+ want to adjust the set of fixer
rules
+ that 2to3 applies:
+
+CALL convert_python3_all(options => '-f idioms -x apply');
+
+ See 2to3's
+ documentation
+ for more information.
+
+
+
+ The convert_python3 extension also provides a
+ procedure that converts just one Python 2 function at a time:
+
+CALL convert_python3_one('myfunc(int)');
+
+ The argument is the target function's OID, which can be written as
+ a regprocedure constant (see
+ ). The main reason to use this would be
+ if you need to use different options for different functions. It has
+ the same optional arguments as convert_python3_all:
+
+CALL convert_python3_one('otherfunc(text)', tool => '/usr/bin/2to3',
+ options => '-f idioms');
+
+
+
+
+ If you have needs that go beyond this, consult the source code for
+ the convert_python3 extension (it's just a
+ couple of plpython3u procedures) and adapt those
+ procedures as necessary.
+
+
+
+ Keep in mind that if you've constructed any DO blocks
+ that use Python 2 code, those will have to be fixed up manually,
+ wherever the source code for them exists.
+
+
diff --git a/src/pl/plpython/Makefile b/src/pl/plpython/Makefile
index 9e95285..03f858b 100644
--- a/src/pl/plpython/Makefile
+++ b/src/pl/plpython/Makefile
@@ -38,6 +38,9 @@ DATA = $(NAME)u.control $(NAME)u--1.0.sql
ifeq ($(python_majorversion),2)
DATA += plpythonu.control plpythonu--1.0.sql
endif
+ifeq ($(python_majorversion),3)
+DATA += convert_python3.control convert_python3--1.0.sql
+endif
# header files to install - it's not clear which of these might be needed
# so install them all.
diff --git a/src/pl/plpython/convert_python3--1.0.sql b/src/pl/plpython/convert_python3--1.0.sql
new file mode 100644
index 0000000..3444ac0
--- /dev/null
+++ b/src/pl/plpython/convert_python3--1.0.sql
@@ -0,0 +1,149 @@
+/* convert_python3--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION convert_python3" to load this file. \quit
+
+-- This module provides two procedures, one to convert all python2
+-- functions and one to do just one. They're nearly identical, and
+-- in principle convert_python3_all() could be written as a loop
+-- around convert_python3_one(). It's not done that way since
+-- creating a temp directory for each function in a bulk conversion
+-- could get expensive.
+
+-- For some benighted reason, lib2to3 has exactly no documented API,
+-- so we must use the command-line API "2to3" instead. User may pass
+-- in the name of that program (in case it's not in the server's PATH)
+-- as well as any desired options for it (perhaps some -f switches).
+
+create procedure convert_python3_all(tool text default '2to3',
+ options text default '')
+language plpython3u as $$
+import re, subprocess, tempfile
+
+# pattern to extract just the function header from pg_get_functiondef result
+aspat = re.compile("^(.*?\nAS )", re.DOTALL)
+# pattern for replacing LANGUAGE portion
+langpat = re.compile("\n LANGUAGE plpython2?u\n")
+
+# collect info about functions to update
+rv = plpy.execute("""
+select p.oid::pg_catalog.regprocedure as funcid,
+ pg_catalog.pg_get_functiondef(p.oid) as fd,
+ prosrc as body
+from pg_catalog.pg_proc p join pg_catalog.pg_language l on p.prolang = l.oid
+where lanname in ('plpythonu', 'plpython2u')
+order by proname
+""")
+
+# Make a temp directory to hold the file for 2to3 to work on.
+with tempfile.TemporaryDirectory() as tmpdirname:
+
+ # process each function
+ for r in rv:
+ # emit notices so user can tell which function failed, if one does
+ plpy.notice("converting function " + r["funcid"])
+
+ # extract everything but the body from pg_get_functiondef result
+ m = aspat.match(r["fd"])
+ if not m:
+ raise ValueError('unexpected match failure')
+ fheader = m.group(1)
+
+ # replace the language clause
+ fheader = langpat.sub("\n LANGUAGE plpython3u\n", fheader, 1)
+
+ # put body in a temp file so we can apply 2to3
+ f = open(tmpdirname + "/temp.py", mode = 'w')
+ f.write(r["body"])
+ f.close()
+
+ # apply 2to3 to body
+ subprocess.check_call(tool + " " + options + " --no-diffs -w " + tmpdirname + "/temp.py", shell=True)
+ f = open(tmpdirname + "/temp.py", mode = 'r')
+ fbody = f.read()
+ f.close()
+
+ # ensure check_function_bodies is enabled
+ plpy.execute("set local check_function_bodies = true")
+
+ # construct and execute SQL command to replace the function
+ newstmt = fheader + plpy.quote_literal(fbody)
+ # uncomment this for debugging purposes:
+ # plpy.info(newstmt)
+ plpy.execute(newstmt)
+
+ # commit after each successful replacement, in case a later one fails
+ plpy.commit()
+$$;
+
+-- The above procedure has to be superuser-only since it trivially allows
+-- executing random programs. But you'd have to be superuser anyway
+-- to replace the definitions of plpython functions.
+
+revoke all on procedure convert_python3_all(text, text) from public;
+
+
+-- Here's the one-function version.
+
+create procedure convert_python3_one(funcid regprocedure,
+ tool text default '2to3',
+ options text default '')
+language plpython3u as $$
+import re, subprocess, tempfile
+
+# pattern to extract just the function header from pg_get_functiondef result
+aspat = re.compile("^(.*?\nAS )", re.DOTALL)
+# pattern for replacing LANGUAGE portion
+langpat = re.compile("\n LANGUAGE plpython2?u\n")
+
+# collect info about function to update, making sure it's the right language
+plan = plpy.prepare("""
+select p.oid::pg_catalog.regprocedure as funcid,
+ pg_catalog.pg_get_functiondef(p.oid) as fd,
+ prosrc as body
+from pg_catalog.pg_proc p join pg_catalog.pg_language l on p.prolang = l.oid
+where p.oid = $1 and lanname in ('plpythonu', 'plpython2u')
+""", ["pg_catalog.regprocedure"])
+
+rv = plpy.execute(plan, [funcid])
+
+# Make a temp directory to hold the file for 2to3 to work on.
+with tempfile.TemporaryDirectory() as tmpdirname:
+
+ # process each function (we only expect one, but it's easy to loop)
+ for r in rv:
+ # extract everything but the body from pg_get_functiondef result
+ m = aspat.match(r["fd"])
+ if not m:
+ raise ValueError('unexpected match failure')
+ fheader = m.group(1)
+
+ # replace the language clause
+ fheader = langpat.sub("\n LANGUAGE plpython3u\n", fheader, 1)
+
+ # put body in a temp file so we can apply 2to3
+ f = open(tmpdirname + "/temp.py", mode = 'w')
+ f.write(r["body"])
+ f.close()
+
+ # apply 2to3 to body
+ subprocess.check_call(tool + " " + options + " --no-diffs -w " + tmpdirname + "/temp.py", shell=True)
+ f = open(tmpdirname + "/temp.py", mode = 'r')
+ fbody = f.read()
+ f.close()
+
+ # ensure check_function_bodies is enabled
+ plpy.execute("set local check_function_bodies = true")
+
+ # construct and execute SQL command to replace the function
+ newstmt = fheader + plpy.quote_literal(fbody)
+ # uncomment this for debugging purposes:
+ # plpy.info(newstmt)
+ plpy.execute(newstmt)
+$$;
+
+-- The above procedure has to be superuser-only since it trivially allows
+-- executing random programs. But you'd have to be superuser anyway
+-- to replace the definitions of plpython functions.
+
+revoke all on procedure convert_python3_one(regprocedure, text, text) from public;
diff --git a/src/pl/plpython/convert_python3.control b/src/pl/plpython/convert_python3.control
new file mode 100644
index 0000000..8debb3b
--- /dev/null
+++ b/src/pl/plpython/convert_python3.control
@@ -0,0 +1,5 @@
+# convert_python3 extension
+comment = 'convert plpython[2]u functions to plpython3u'
+default_version = '1.0'
+relocatable = true
+requires = 'plpython3u'