[Python-3000-checkins] r65559 - in python/branches/py3k: Lib/test/test_urllib.py Lib/urllib/parse.py Modules/_sre.c
Guido van Rossum
guido at python.org
Wed Aug 6 21:32:25 CEST 2008
Yes, I know the two Lib files shouldn't have been committed. I've
already reverted them, r66650.
On Wed, Aug 6, 2008 at 12:29 PM, guido.van.rossum
<python-3000-checkins at python.org> wrote:
> Author: guido.van.rossum
> Date: Wed Aug 6 21:29:14 2008
> New Revision: 65559
>
> Log:
> Merged revisions 65544 via svnmerge from
> svn+ssh://pythondev@svn.python.org/python/trunk
>
> ........
> r65544 | guido.van.rossum | 2008-08-04 20:39:21 -0700 (Mon, 04 Aug 2008) | 28 lines
>
> Tracker issue 3487: sre "bytecode" verifier.
>
> This is a verifier for the binary code used by the _sre module (this
> is often called bytecode, though to distinguish it from Python bytecode
> I put it in quotes).
>
> I wrote this for Google App Engine, and am making the patch available as
> open source under the Apache 2 license. Below are the copyright
> statement and license, for completeness.
>
> # Copyright 2008 Google Inc.
> #
> # Licensed under the Apache License, Version 2.0 (the "License");
> # you may not use this file except in compliance with the License.
> # You may obtain a copy of the License at
> #
> # http://www.apache.org/licenses/LICENSE-2.0
> #
> # Unless required by applicable law or agreed to in writing, software
> # distributed under the License is distributed on an "AS IS" BASIS,
> # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
> # See the License for the specific language governing permissions and
> # limitations under the License.
>
> It's not necessary to include these copyrights and bytecode in the
> source file. Google has signed a contributor's agreement with the PSF
> already.
> ........
>
>
> Modified:
> python/branches/py3k/ (props changed)
> python/branches/py3k/Lib/test/test_urllib.py
> python/branches/py3k/Lib/urllib/parse.py
> python/branches/py3k/Modules/_sre.c
>
> Modified: python/branches/py3k/Lib/test/test_urllib.py
> ==============================================================================
> --- python/branches/py3k/Lib/test/test_urllib.py (original)
> +++ python/branches/py3k/Lib/test/test_urllib.py Wed Aug 6 21:29:14 2008
> @@ -465,7 +465,7 @@
>
> def test_unquote_with_unicode(self):
> r = urllib.parse.unquote('br%C3%BCckner_sapporo_20050930.doc')
> - self.assertEqual(r, 'br\xc3\xbcckner_sapporo_20050930.doc')
> + self.assertEqual(r, 'br\u00FCckner_sapporo_20050930.doc')
>
> class urlencode_Tests(unittest.TestCase):
> """Tests for urlencode()"""
>
> Modified: python/branches/py3k/Lib/urllib/parse.py
> ==============================================================================
> --- python/branches/py3k/Lib/urllib/parse.py (original)
> +++ python/branches/py3k/Lib/urllib/parse.py Wed Aug 6 21:29:14 2008
> @@ -261,84 +261,74 @@
> return url, ''
>
>
> -_hextochr = dict(('%02x' % i, chr(i)) for i in range(256))
> -_hextochr.update(('%02X' % i, chr(i)) for i in range(256))
> +def unquote_as_string (s, plus=False, charset=None):
> + if charset is None:
> + charset = "UTF-8"
> + return str(unquote_as_bytes(s, plus=plus), charset, 'strict')
>
> -def unquote(s):
> +def unquote_as_bytes (s, plus=False):
> """unquote('abc%20def') -> 'abc def'."""
> + if plus:
> + s = s.replace('+', ' ')
> res = s.split('%')
> + res[0] = res[0].encode('ASCII', 'strict')
> for i in range(1, len(res)):
> - item = res[i]
> - try:
> - res[i] = _hextochr[item[:2]] + item[2:]
> - except KeyError:
> - res[i] = '%' + item
> - except UnicodeDecodeError:
> - res[i] = chr(int(item[:2], 16)) + item[2:]
> - return "".join(res)
> -
> -def unquote_plus(s):
> - """unquote('%7e/abc+def') -> '~/abc def'"""
> - s = s.replace('+', ' ')
> - return unquote(s)
> -
> -always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
> - 'abcdefghijklmnopqrstuvwxyz'
> - '0123456789' '_.-')
> -_safe_quoters= {}
> -
> -class Quoter:
> - def __init__(self, safe):
> - self.cache = {}
> - self.safe = safe + always_safe
> + res[i] = (bytes.fromhex(res[i][:2]) +
> + res[i][2:].encode('ASCII', 'strict'))
> + return b''.join(res)
> +
> +_always_safe = (b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
> + b'abcdefghijklmnopqrstuvwxyz'
> + b'0123456789'
> + b'_.-')
> +
> +_percent_code = ord('%')
> +
> +_hextable = b'0123456789ABCDEF'
> +
> +def quote_as_bytes(s, safe = '/', plus=False):
> + """quote(b'abc at def') -> 'abc%40def'"""
> +
> + if isinstance(s, str):
> + s = s.encode("UTF-8", "strict")
> + if not (isinstance(s, bytes) or isinstance(s, bytearray)):
> + raise ValueError("Argument to quote must be either bytes "
> + "or bytearray; string arguments will be "
> + "converted to UTF-8 bytes")
> +
> + safeset = _always_safe + safe.encode('ASCII', 'strict')
> + if plus:
> + safeset += b' '
> +
> + result = bytearray()
> + for i in s:
> + if i not in safeset:
> + result.append(_percent_code)
> + result.append(_hextable[(i >> 4) & 0xF])
> + result.append(_hextable[i & 0xF])
> + else:
> + result.append(i)
> + if plus:
> + result = result.replace(b' ', b'+')
> + return result
>
> - def __call__(self, c):
> - try:
> - return self.cache[c]
> - except KeyError:
> - if ord(c) < 256:
> - res = (c in self.safe) and c or ('%%%02X' % ord(c))
> - self.cache[c] = res
> - return res
> - else:
> - return "".join(['%%%02X' % i for i in c.encode("utf-8")])
> +def quote_as_string(s, safe = '/', plus=False):
> + return str(quote_as_bytes(s, safe=safe, plus=plus), 'ASCII', 'strict')
>
> -def quote(s, safe = '/'):
> - """quote('abc def') -> 'abc%20def'
> +# finally, define defaults for 'quote' and 'unquote'
>
> - Each part of a URL, e.g. the path info, the query, etc., has a
> - different set of reserved characters that must be quoted.
> +def quote(s, safe='/'):
> + return quote_as_string(s, safe=safe)
>
> - RFC 2396 Uniform Resource Identifiers (URI): Generic Syntax lists
> - the following reserved characters.
> +def quote_plus(s, safe=''):
> + return quote_as_string(s, safe=safe, plus=True)
>
> - reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
> - "$" | ","
> +def unquote(s):
> + return unquote_as_string(s)
>
> - Each of these characters is reserved in some component of a URL,
> - but not necessarily in all of them.
> +def unquote_plus(s):
> + return unquote_as_string(s, plus=True)
>
> - By default, the quote function is intended for quoting the path
> - section of a URL. Thus, it will not encode '/'. This character
> - is reserved, but in typical usage the quote function is being
> - called on a path where the existing slash characters are used as
> - reserved characters.
> - """
> - cachekey = (safe, always_safe)
> - try:
> - quoter = _safe_quoters[cachekey]
> - except KeyError:
> - quoter = Quoter(safe)
> - _safe_quoters[cachekey] = quoter
> - res = map(quoter, s)
> - return ''.join(res)
> -
> -def quote_plus(s, safe = ''):
> - """Quote the query fragment of a URL; replacing ' ' with '+'"""
> - if ' ' in s:
> - s = quote(s, safe + ' ')
> - return s.replace(' ', '+')
> - return quote(s, safe)
>
> def urlencode(query,doseq=0):
> """Encode a sequence of two-element tuples or dictionary into a URL query string.
> @@ -387,7 +377,7 @@
> # is there a reasonable way to convert to ASCII?
> # encode generates a string, but "replace" or "ignore"
> # lose information and "strict" can raise UnicodeError
> - v = quote_plus(v.encode("ASCII","replace"))
> + v = quote_plus(v)
> l.append(k + '=' + v)
> else:
> try:
> @@ -474,7 +464,8 @@
> _userprog = re.compile('^(.*)@(.*)$')
>
> match = _userprog.match(host)
> - if match: return map(unquote, match.group(1, 2))
> + if match:
> + return map(unquote, match.group(1, 2))
> return None, host
>
> _passwdprog = None
>
> Modified: python/branches/py3k/Modules/_sre.c
> ==============================================================================
> --- python/branches/py3k/Modules/_sre.c (original)
> +++ python/branches/py3k/Modules/_sre.c Wed Aug 6 21:29:14 2008
> @@ -2637,6 +2637,8 @@
> pattern_members, /* tp_members */
> };
>
> +static int _validate(PatternObject *self); /* Forward */
> +
> static PyObject *
> _compile(PyObject* self_, PyObject* args)
> {
> @@ -2695,10 +2697,482 @@
>
> self->weakreflist = NULL;
>
> + if (!_validate(self)) {
> + Py_DECREF(self);
> + return NULL;
> + }
> +
> return (PyObject*) self;
> }
>
> /* -------------------------------------------------------------------- */
> +/* Code validation */
> +
> +/* To learn more about this code, have a look at the _compile() function in
> + Lib/sre_compile.py. The validation functions below checks the code array
> + for conformance with the code patterns generated there.
> +
> + The nice thing about the generated code is that it is position-independent:
> + all jumps are relative jumps forward. Also, jumps don't cross each other:
> + the target of a later jump is always earlier than the target of an earlier
> + jump. IOW, this is okay:
> +
> + J---------J-------T--------T
> + \ \_____/ /
> + \______________________/
> +
> + but this is not:
> +
> + J---------J-------T--------T
> + \_________\_____/ /
> + \____________/
> +
> + It also helps that SRE_CODE is always an unsigned type, either 2 bytes or 4
> + bytes wide (the latter if Python is compiled for "wide" unicode support).
> +*/
> +
> +/* Defining this one enables tracing of the validator */
> +#undef VVERBOSE
> +
> +/* Trace macro for the validator */
> +#if defined(VVERBOSE)
> +#define VTRACE(v) printf v
> +#else
> +#define VTRACE(v)
> +#endif
> +
> +/* Report failure */
> +#define FAIL do { VTRACE(("FAIL: %d\n", __LINE__)); return 0; } while (0)
> +
> +/* Extract opcode, argument, or skip count from code array */
> +#define GET_OP \
> + do { \
> + VTRACE(("%p: ", code)); \
> + if (code >= end) FAIL; \
> + op = *code++; \
> + VTRACE(("%lu (op)\n", (unsigned long)op)); \
> + } while (0)
> +#define GET_ARG \
> + do { \
> + VTRACE(("%p= ", code)); \
> + if (code >= end) FAIL; \
> + arg = *code++; \
> + VTRACE(("%lu (arg)\n", (unsigned long)arg)); \
> + } while (0)
> +#define GET_SKIP \
> + do { \
> + VTRACE(("%p= ", code)); \
> + if (code >= end) FAIL; \
> + skip = *code; \
> + VTRACE(("%lu (skip to %p)\n", \
> + (unsigned long)skip, code+skip)); \
> + if (code+skip < code || code+skip > end) \
> + FAIL; \
> + code++; \
> + } while (0)
> +
> +static int
> +_validate_charset(SRE_CODE *code, SRE_CODE *end)
> +{
> + /* Some variables are manipulated by the macros above */
> + SRE_CODE op;
> + SRE_CODE arg;
> + SRE_CODE offset;
> + int i;
> +
> + while (code < end) {
> + GET_OP;
> + switch (op) {
> +
> + case SRE_OP_NEGATE:
> + break;
> +
> + case SRE_OP_LITERAL:
> + GET_ARG;
> + break;
> +
> + case SRE_OP_RANGE:
> + GET_ARG;
> + GET_ARG;
> + break;
> +
> + case SRE_OP_CHARSET:
> + offset = 32/sizeof(SRE_CODE); /* 32-byte bitmap */
> + if (code+offset < code || code+offset > end)
> + FAIL;
> + code += offset;
> + break;
> +
> + case SRE_OP_BIGCHARSET:
> + GET_ARG; /* Number of blocks */
> + offset = 256/sizeof(SRE_CODE); /* 256-byte table */
> + if (code+offset < code || code+offset > end)
> + FAIL;
> + /* Make sure that each byte points to a valid block */
> + for (i = 0; i < 256; i++) {
> + if (((unsigned char *)code)[i] >= arg)
> + FAIL;
> + }
> + code += offset;
> + offset = arg * 32/sizeof(SRE_CODE); /* 32-byte bitmap times arg */
> + if (code+offset < code || code+offset > end)
> + FAIL;
> + code += offset;
> + break;
> +
> + case SRE_OP_CATEGORY:
> + GET_ARG;
> + switch (arg) {
> + case SRE_CATEGORY_DIGIT:
> + case SRE_CATEGORY_NOT_DIGIT:
> + case SRE_CATEGORY_SPACE:
> + case SRE_CATEGORY_NOT_SPACE:
> + case SRE_CATEGORY_WORD:
> + case SRE_CATEGORY_NOT_WORD:
> + case SRE_CATEGORY_LINEBREAK:
> + case SRE_CATEGORY_NOT_LINEBREAK:
> + case SRE_CATEGORY_LOC_WORD:
> + case SRE_CATEGORY_LOC_NOT_WORD:
> + case SRE_CATEGORY_UNI_DIGIT:
> + case SRE_CATEGORY_UNI_NOT_DIGIT:
> + case SRE_CATEGORY_UNI_SPACE:
> + case SRE_CATEGORY_UNI_NOT_SPACE:
> + case SRE_CATEGORY_UNI_WORD:
> + case SRE_CATEGORY_UNI_NOT_WORD:
> + case SRE_CATEGORY_UNI_LINEBREAK:
> + case SRE_CATEGORY_UNI_NOT_LINEBREAK:
> + break;
> + default:
> + FAIL;
> + }
> + break;
> +
> + default:
> + FAIL;
> +
> + }
> + }
> +
> + return 1;
> +}
> +
> +static int
> +_validate_inner(SRE_CODE *code, SRE_CODE *end, Py_ssize_t groups)
> +{
> + /* Some variables are manipulated by the macros above */
> + SRE_CODE op;
> + SRE_CODE arg;
> + SRE_CODE skip;
> +
> + VTRACE(("code=%p, end=%p\n", code, end));
> +
> + if (code > end)
> + FAIL;
> +
> + while (code < end) {
> + GET_OP;
> + switch (op) {
> +
> + case SRE_OP_MARK:
> + /* We don't check whether marks are properly nested; the
> + sre_match() code is robust even if they don't, and the worst
> + you can get is nonsensical match results. */
> + GET_ARG;
> + if (arg > 2*groups+1) {
> + VTRACE(("arg=%d, groups=%d\n", (int)arg, (int)groups));
> + FAIL;
> + }
> + break;
> +
> + case SRE_OP_LITERAL:
> + case SRE_OP_NOT_LITERAL:
> + case SRE_OP_LITERAL_IGNORE:
> + case SRE_OP_NOT_LITERAL_IGNORE:
> + GET_ARG;
> + /* The arg is just a character, nothing to check */
> + break;
> +
> + case SRE_OP_SUCCESS:
> + case SRE_OP_FAILURE:
> + /* Nothing to check; these normally end the matching process */
> + break;
> +
> + case SRE_OP_AT:
> + GET_ARG;
> + switch (arg) {
> + case SRE_AT_BEGINNING:
> + case SRE_AT_BEGINNING_STRING:
> + case SRE_AT_BEGINNING_LINE:
> + case SRE_AT_END:
> + case SRE_AT_END_LINE:
> + case SRE_AT_END_STRING:
> + case SRE_AT_BOUNDARY:
> + case SRE_AT_NON_BOUNDARY:
> + case SRE_AT_LOC_BOUNDARY:
> + case SRE_AT_LOC_NON_BOUNDARY:
> + case SRE_AT_UNI_BOUNDARY:
> + case SRE_AT_UNI_NON_BOUNDARY:
> + break;
> + default:
> + FAIL;
> + }
> + break;
> +
> + case SRE_OP_ANY:
> + case SRE_OP_ANY_ALL:
> + /* These have no operands */
> + break;
> +
> + case SRE_OP_IN:
> + case SRE_OP_IN_IGNORE:
> + GET_SKIP;
> + /* Stop 1 before the end; we check the FAILURE below */
> + if (!_validate_charset(code, code+skip-2))
> + FAIL;
> + if (code[skip-2] != SRE_OP_FAILURE)
> + FAIL;
> + code += skip-1;
> + break;
> +
> + case SRE_OP_INFO:
> + {
> + /* A minimal info field is
> + <INFO> <1=skip> <2=flags> <3=min> <4=max>;
> + If SRE_INFO_PREFIX or SRE_INFO_CHARSET is in the flags,
> + more follows. */
> + SRE_CODE flags, min, max, i;
> + SRE_CODE *newcode;
> + GET_SKIP;
> + newcode = code+skip-1;
> + GET_ARG; flags = arg;
> + GET_ARG; min = arg;
> + GET_ARG; max = arg;
> + /* Check that only valid flags are present */
> + if ((flags & ~(SRE_INFO_PREFIX |
> + SRE_INFO_LITERAL |
> + SRE_INFO_CHARSET)) != 0)
> + FAIL;
> + /* PREFIX and CHARSET are mutually exclusive */
> + if ((flags & SRE_INFO_PREFIX) &&
> + (flags & SRE_INFO_CHARSET))
> + FAIL;
> + /* LITERAL implies PREFIX */
> + if ((flags & SRE_INFO_LITERAL) &&
> + !(flags & SRE_INFO_PREFIX))
> + FAIL;
> + /* Validate the prefix */
> + if (flags & SRE_INFO_PREFIX) {
> + SRE_CODE prefix_len, prefix_skip;
> + GET_ARG; prefix_len = arg;
> + GET_ARG; prefix_skip = arg;
> + /* Here comes the prefix string */
> + if (code+prefix_len < code || code+prefix_len > newcode)
> + FAIL;
> + code += prefix_len;
> + /* And here comes the overlap table */
> + if (code+prefix_len < code || code+prefix_len > newcode)
> + FAIL;
> + /* Each overlap value should be < prefix_len */
> + for (i = 0; i < prefix_len; i++) {
> + if (code[i] >= prefix_len)
> + FAIL;
> + }
> + code += prefix_len;
> + }
> + /* Validate the charset */
> + if (flags & SRE_INFO_CHARSET) {
> + if (!_validate_charset(code, newcode-1))
> + FAIL;
> + if (newcode[-1] != SRE_OP_FAILURE)
> + FAIL;
> + code = newcode;
> + }
> + else if (code != newcode) {
> + VTRACE(("code=%p, newcode=%p\n", code, newcode));
> + FAIL;
> + }
> + }
> + break;
> +
> + case SRE_OP_BRANCH:
> + {
> + SRE_CODE *target = NULL;
> + for (;;) {
> + GET_SKIP;
> + if (skip == 0)
> + break;
> + /* Stop 2 before the end; we check the JUMP below */
> + if (!_validate_inner(code, code+skip-3, groups))
> + FAIL;
> + code += skip-3;
> + /* Check that it ends with a JUMP, and that each JUMP
> + has the same target */
> + GET_OP;
> + if (op != SRE_OP_JUMP)
> + FAIL;
> + GET_SKIP;
> + if (target == NULL)
> + target = code+skip-1;
> + else if (code+skip-1 != target)
> + FAIL;
> + }
> + }
> + break;
> +
> + case SRE_OP_REPEAT_ONE:
> + case SRE_OP_MIN_REPEAT_ONE:
> + {
> + SRE_CODE min, max;
> + GET_SKIP;
> + GET_ARG; min = arg;
> + GET_ARG; max = arg;
> + if (min > max)
> + FAIL;
> +#ifdef Py_UNICODE_WIDE
> + if (max > 65535)
> + FAIL;
> +#endif
> + if (!_validate_inner(code, code+skip-4, groups))
> + FAIL;
> + code += skip-4;
> + GET_OP;
> + if (op != SRE_OP_SUCCESS)
> + FAIL;
> + }
> + break;
> +
> + case SRE_OP_REPEAT:
> + {
> + SRE_CODE min, max;
> + GET_SKIP;
> + GET_ARG; min = arg;
> + GET_ARG; max = arg;
> + if (min > max)
> + FAIL;
> +#ifdef Py_UNICODE_WIDE
> + if (max > 65535)
> + FAIL;
> +#endif
> + if (!_validate_inner(code, code+skip-3, groups))
> + FAIL;
> + code += skip-3;
> + GET_OP;
> + if (op != SRE_OP_MAX_UNTIL && op != SRE_OP_MIN_UNTIL)
> + FAIL;
> + }
> + break;
> +
> + case SRE_OP_GROUPREF:
> + case SRE_OP_GROUPREF_IGNORE:
> + GET_ARG;
> + if (arg >= groups)
> + FAIL;
> + break;
> +
> + case SRE_OP_GROUPREF_EXISTS:
> + /* The regex syntax for this is: '(?(group)then|else)', where
> + 'group' is either an integer group number or a group name,
> + 'then' and 'else' are sub-regexes, and 'else' is optional. */
> + GET_ARG;
> + if (arg >= groups)
> + FAIL;
> + GET_SKIP;
> + code--; /* The skip is relative to the first arg! */
> + /* There are two possibilities here: if there is both a 'then'
> + part and an 'else' part, the generated code looks like:
> +
> + GROUPREF_EXISTS
> + <group>
> + <skipyes>
> + ...then part...
> + JUMP
> + <skipno>
> + (<skipyes> jumps here)
> + ...else part...
> + (<skipno> jumps here)
> +
> + If there is only a 'then' part, it looks like:
> +
> + GROUPREF_EXISTS
> + <group>
> + <skip>
> + ...then part...
> + (<skip> jumps here)
> +
> + There is no direct way to decide which it is, and we don't want
> + to allow arbitrary jumps anywhere in the code; so we just look
> + for a JUMP opcode preceding our skip target.
> + */
> + if (skip >= 3 && code+skip-3 >= code &&
> + code[skip-3] == SRE_OP_JUMP)
> + {
> + VTRACE(("both then and else parts present\n"));
> + if (!_validate_inner(code+1, code+skip-3, groups))
> + FAIL;
> + code += skip-2; /* Position after JUMP, at <skipno> */
> + GET_SKIP;
> + if (!_validate_inner(code, code+skip-1, groups))
> + FAIL;
> + code += skip-1;
> + }
> + else {
> + VTRACE(("only a then part present\n"));
> + if (!_validate_inner(code+1, code+skip-1, groups))
> + FAIL;
> + code += skip-1;
> + }
> + break;
> +
> + case SRE_OP_ASSERT:
> + case SRE_OP_ASSERT_NOT:
> + GET_SKIP;
> + GET_ARG; /* 0 for lookahead, width for lookbehind */
> + code--; /* Back up over arg to simplify math below */
> + if (arg & 0x80000000)
> + FAIL; /* Width too large */
> + /* Stop 1 before the end; we check the SUCCESS below */
> + if (!_validate_inner(code+1, code+skip-2, groups))
> + FAIL;
> + code += skip-2;
> + GET_OP;
> + if (op != SRE_OP_SUCCESS)
> + FAIL;
> + break;
> +
> + default:
> + FAIL;
> +
> + }
> + }
> +
> + VTRACE(("okay\n"));
> + return 1;
> +}
> +
> +static int
> +_validate_outer(SRE_CODE *code, SRE_CODE *end, Py_ssize_t groups)
> +{
> + if (groups < 0 || groups > 100 || code >= end || end[-1] != SRE_OP_SUCCESS)
> + FAIL;
> + if (groups == 0) /* fix for simplejson */
> + groups = 100; /* 100 groups should always be safe */
> + return _validate_inner(code, end-1, groups);
> +}
> +
> +static int
> +_validate(PatternObject *self)
> +{
> + if (!_validate_outer(self->code, self->code+self->codesize, self->groups))
> + {
> + PyErr_SetString(PyExc_RuntimeError, "invalid SRE code");
> + return 0;
> + }
> + else
> + VTRACE(("Success!\n"));
> + return 1;
> +}
> +
> +/* -------------------------------------------------------------------- */
> /* match methods */
>
> static void
> _______________________________________________
> Python-3000-checkins mailing list
> Python-3000-checkins at python.org
> http://mail.python.org/mailman/listinfo/python-3000-checkins
>
--
--Guido van Rossum (home page: http://www.python.org/~guido/)
More information about the Python-3000-checkins
mailing list