3 new commits in py:
https://bitbucket.org/hpk42/py/changeset/e6cf2e2fee51/ changeset: e6cf2e2fee51 user: hpk42 date: 2012-10-25 09:44:01 summary: convert terminal write-data to unicode if it is not a string/bytes affected #: 6 files diff -r 7e29aba2f9791e942b87149cbb0c4812a7bf2f0a -r e6cf2e2fee5156e174a547beeb7d827d723a944b CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,8 @@ - fix a doc link to bug tracker - try to make terminal.write() printing more robust against unicodeencode/decode problems, amend according test +- introduce py.builtin.text and py.builtin.bytes + to point to respective str/unicode (py2) and bytes/str (py3) types Changes between 1.4.9 and 1.4.10 ================================================== diff -r 7e29aba2f9791e942b87149cbb0c4812a7bf2f0a -r e6cf2e2fee5156e174a547beeb7d827d723a944b py/__init__.py --- a/py/__init__.py +++ b/py/__init__.py @@ -8,7 +8,7 @@ (c) Holger Krekel and others, 2004-2010 """ -__version__ = '1.4.11.dev2' +__version__ = '1.4.11.dev4' from py import _apipkg @@ -104,6 +104,8 @@ 'builtins' : '._builtin:builtins', 'execfile' : '._builtin:execfile', 'callable' : '._builtin:callable', + 'bytes' : '._builtin:bytes', + 'text' : '._builtin:text', }, # input-output helping diff -r 7e29aba2f9791e942b87149cbb0c4812a7bf2f0a -r e6cf2e2fee5156e174a547beeb7d827d723a944b py/_builtin.py --- a/py/_builtin.py +++ b/py/_builtin.py @@ -128,6 +128,10 @@ def _istext(x): return isinstance(x, str) + text = str + bytes = bytes + + def _getimself(function): return getattr(function, '__self__', None) @@ -160,6 +164,8 @@ import __builtin__ as builtins _totext = unicode _basestring = basestring + text = unicode + bytes = str execfile = execfile callable = callable def _isbytes(x): diff -r 7e29aba2f9791e942b87149cbb0c4812a7bf2f0a -r e6cf2e2fee5156e174a547beeb7d827d723a944b py/_io/terminalwriter.py --- a/py/_io/terminalwriter.py +++ b/py/_io/terminalwriter.py @@ -8,6 +8,7 @@ import sys, os import py py3k = sys.version_info[0] >= 3 +from py.builtin import text, bytes win32_and_ctypes = False if sys.platform == "win32": @@ -163,6 +164,8 @@ def write(self, msg, **kw): if msg: + if not isinstance(msg, (bytes, text)): + msg = text(msg) if self.hasmarkup and kw: markupmsg = self.markup(msg, **kw) else: diff -r 7e29aba2f9791e942b87149cbb0c4812a7bf2f0a -r e6cf2e2fee5156e174a547beeb7d827d723a944b setup.py --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ name='py', description='library with cross-python path, ini-parsing, io, code, log facilities', long_description = open('README.txt').read(), - version='1.4.11.dev2', + version='1.4.11.dev4', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff -r 7e29aba2f9791e942b87149cbb0c4812a7bf2f0a -r e6cf2e2fee5156e174a547beeb7d827d723a944b testing/root/test_builtin.py --- a/testing/root/test_builtin.py +++ b/testing/root/test_builtin.py @@ -133,6 +133,14 @@ def test_totext(): py.builtin._totext("hello", "UTF-8") +def test_bytes_text(): + if sys.version_info[0] < 3: + assert py.builtin.text == unicode + assert py.builtin.bytes == str + else: + assert py.builtin.text == str + assert py.builtin.bytes == bytes + def test_totext_badutf8(): # this was in printouts within the pytest testsuite # totext would fail https://bitbucket.org/hpk42/py/changeset/2ccc7c44763d/ changeset: 2ccc7c44763d user: hpk42 date: 2012-10-25 11:15:01 summary: fix pytest issue207 rewrite heuristic to find statements to use _ast module affected #: 6 files diff -r e6cf2e2fee5156e174a547beeb7d827d723a944b -r 2ccc7c44763d32e794d46e12933053c07962eb52 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ Changes between 1.4.10 and 1.4.11.dev ================================================== +- use _ast to determine statement ranges when printing tracebacks - + avoids multi-second delays on some large test modules - fix an internal test to not use class-denoted pytest_funcarg__ - fix a doc link to bug tracker - try to make terminal.write() printing more robust against diff -r e6cf2e2fee5156e174a547beeb7d827d723a944b -r 2ccc7c44763d32e794d46e12933053c07962eb52 py/__init__.py --- a/py/__init__.py +++ b/py/__init__.py @@ -8,7 +8,7 @@ (c) Holger Krekel and others, 2004-2010 """ -__version__ = '1.4.11.dev4' +__version__ = '1.4.11.dev5' from py import _apipkg diff -r e6cf2e2fee5156e174a547beeb7d827d723a944b -r 2ccc7c44763d32e794d46e12933053c07962eb52 py/_code/code.py --- a/py/_code/code.py +++ b/py/_code/code.py @@ -166,20 +166,12 @@ if source is None: return None start = self.getfirstlinesource() - end = self.lineno try: - _, end = source.getstatementrange(end) - except (IndexError, ValueError): + _, end = source.getstatementrange(self.lineno) + except SyntaxError: end = self.lineno + 1 - # heuristic to stop displaying source on e.g. - # if something: # assume this causes a NameError - # # _this_ lines and the one - # below we don't want from entry.getsource() - for i in range(self.lineno, end): - if source[i].rstrip().endswith(':'): - end = i + 1 - break return source[start:end] + source = property(getsource) def ishidden(self): diff -r e6cf2e2fee5156e174a547beeb7d827d723a944b -r 2ccc7c44763d32e794d46e12933053c07962eb52 py/_code/source.py --- a/py/_code/source.py +++ b/py/_code/source.py @@ -108,47 +108,10 @@ def getstatementrange(self, lineno, assertion=False): """ return (start, end) tuple which spans the minimal statement region which containing the given lineno. - raise an IndexError if no such statementrange can be found. """ - # XXX there must be a better than these heuristic ways ... - # XXX there may even be better heuristics :-) if not (0 <= lineno < len(self)): raise IndexError("lineno out of range") - - # 1. find the start of the statement - from codeop import compile_command - for start in range(lineno, -1, -1): - if assertion: - line = self.lines[start] - # the following lines are not fully tested, change with care - if 'super' in line and 'self' in line and '__init__' in line: - raise IndexError("likely a subclass") - if "assert" not in line and "raise" not in line: - continue - trylines = self.lines[start:lineno+1] - # quick hack to prepare parsing an indented line with - # compile_command() (which errors on "return" outside defs) - trylines.insert(0, 'def xxx():') - trysource = '\n '.join(trylines) - # ^ space here - try: - compile_command(trysource) - except (SyntaxError, OverflowError, ValueError): - continue - - # 2. find the end of the statement - for end in range(lineno+1, len(self)+1): - trysource = self[start:end] - if trysource.isparseable(): - return start, end - raise IndexError("no valid source range around line %d " % (lineno,)) - - def getblockend(self, lineno): - # XXX - lines = [x + '\n' for x in self.lines[lineno:]] - blocklines = inspect.getblock(lines) - #print blocklines - return lineno + len(blocklines) - 1 + return getstatementrange_ast(lineno, self) def deindent(self, offset=None): """ return a new source object deindented by offset. @@ -352,3 +315,65 @@ # Add any lines we didn't see. E.g. if an exception was raised. newlines.extend(lines[len(newlines):]) return newlines + +def get_statement_startend(lineno, nodelist): + from bisect import bisect_right, bisect_left + # lineno starts at 0 + nextlineno = None + while 1: + lineno_list = [x.lineno-1 for x in nodelist] # ast indexes start at 1 + insert_index = bisect_right(lineno_list, lineno) + if insert_index >= len(nodelist): + insert_index -= 1 + elif lineno < (nodelist[insert_index].lineno - 1) and insert_index > 0: + insert_index -= 1 + assert lineno >= (nodelist[insert_index].lineno - 1) + nextnode = nodelist[insert_index] + + try: + nextlineno = nodelist[insert_index+1].lineno - 1 + except IndexError: + pass + lastnodelist = nodelist + nodelist = getnodelist(nextnode) + if not nodelist: + start, end = nextnode.lineno-1, nextlineno + start = min(lineno, start) + assert start <= lineno and (end is None or lineno < end) + #print "returning", start, end + return start, end + +def getnodelist(node): + import _ast + l = [] + #print "node", node, "fields", node._fields, "lineno", getattr(node, "lineno", 0) - 1 + for subname in "test", "type", "body", "handlers", "orelse", "finalbody": + attr = getattr(node, subname, None) + if attr is not None: + if isinstance(attr, list): + l.extend(attr) + elif hasattr(attr, "lineno"): + l.append(attr) + #print "returning nodelist", l + return l + +def getstatementrange_ast(lineno, source): + content = "\n".join(source.lines) + #print "compiling lines", len(source.lines) + astnode = compile(content, "source", "exec", 1024) # 1024 for AST + start, end = get_statement_startend(lineno, getnodelist(astnode)) + # we need to correct the end: + # - ast-parsing strips comments + # - else statements do not have a separate lineno + # - there might be empty lines + if end is None: + end = len(source.lines) + while end: + line = source.lines[end-1].lstrip() + if (not line or line.startswith("#") or line.startswith("else:") or + line.startswith("finally:")): + end -= 1 + else: + break + return start, end + diff -r e6cf2e2fee5156e174a547beeb7d827d723a944b -r 2ccc7c44763d32e794d46e12933053c07962eb52 setup.py --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ name='py', description='library with cross-python path, ini-parsing, io, code, log facilities', long_description = open('README.txt').read(), - version='1.4.11.dev4', + version='1.4.11.dev5', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff -r e6cf2e2fee5156e174a547beeb7d827d723a944b -r 2ccc7c44763d32e794d46e12933053c07962eb52 testing/code/test_source.py --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -178,10 +178,12 @@ def test_getstatementrange_triple_quoted(self): #print str(self.source) - source = Source("""''' - '''""") + source = Source("""hello(''' + ''')""") + s = source.getstatement(0) + assert s == str(source) s = source.getstatement(1) - assert eval(str(s)) + assert s == str(source) def test_getstatementrange_within_constructs(self): source = Source("""\ @@ -194,12 +196,13 @@ 42 """) assert len(source) == 7 - assert source.getstatementrange(0) == (0, 7) - assert source.getstatementrange(1) == (1, 5) + # check all lineno's that could occur in a traceback + #assert source.getstatementrange(0) == (0, 7) + #assert source.getstatementrange(1) == (1, 5) assert source.getstatementrange(2) == (2, 3) - assert source.getstatementrange(3) == (1, 5) + assert source.getstatementrange(3) == (3, 4) assert source.getstatementrange(4) == (4, 5) - assert source.getstatementrange(5) == (0, 7) + #assert source.getstatementrange(5) == (0, 7) assert source.getstatementrange(6) == (6, 7) def test_getstatementrange_bug(self): @@ -238,7 +241,7 @@ def test_getstatementrange_with_syntaxerror_issue7(self): source = Source(":") - py.test.raises(IndexError, lambda: source.getstatementrange(0)) + py.test.raises(SyntaxError, lambda: source.getstatementrange(0)) @py.test.mark.skipif("sys.version_info < (2,6)") def test_compile_to_ast(self): @@ -455,3 +458,123 @@ def __call__(self): pass py.test.raises(TypeError, lambda: py.code.Code(Hello)) + + +def getstatement(lineno, source): + from py._code.source import getstatementrange_ast + source = py.code.Source(source, deindent=False) + start, end = getstatementrange_ast(lineno, source) + return source[start:end] + +def test_oneline(): + source = getstatement(0, "raise ValueError") + assert str(source) == "raise ValueError" + +def test_oneline_and_comment(): + source = getstatement(0, "raise ValueError\n#hello") + assert str(source) == "raise ValueError" + +def XXXtest_multiline(): + source = getstatement(0, """\ +raise ValueError( + 23 +) +x = 3 +""") + assert str(source) == "raise ValueError(\n 23\n)" + +class TestTry: + source = """\ +try: + raise ValueError +except Something: + raise IndexError(1) +else: + raise KeyError() +""" + + def test_body(self): + source = getstatement(1, self.source) + assert str(source) == " raise ValueError" + + def test_except_line(self): + source = getstatement(2, self.source) + assert str(source) == "except Something:" + + def test_except_body(self): + source = getstatement(3, self.source) + assert str(source) == " raise IndexError(1)" + + def test_else(self): + source = getstatement(5, self.source) + assert str(source) == " raise KeyError()" + +class TestTryFinally: + source = """\ +try: + raise ValueError +finally: + raise IndexError(1) +""" + + def test_body(self): + source = getstatement(1, self.source) + assert str(source) == " raise ValueError" + + def test_finally(self): + source = getstatement(3, self.source) + assert str(source) == " raise IndexError(1)" + + + +class TestIf: + source = """\ +if 1: + y = 3 +elif False: + y = 5 +else: + y = 7 +""" + + def test_body(self): + source = getstatement(1, self.source) + assert str(source) == " y = 3" + + def test_elif_clause(self): + source = getstatement(2, self.source) + assert str(source) == "elif False:" + + def test_elif(self): + source = getstatement(3, self.source) + assert str(source) == " y = 5" + + def test_else(self): + source = getstatement(5, self.source) + assert str(source) == " y = 7" + +def test_semicolon(): + s = """\ +hello ; pytest.skip() +""" + source = getstatement(0, s) + assert str(source) == s.strip() + +def test_def_online(): + s = """\ +def func(): raise ValueError(42) + +def something(): + pass +""" + source = getstatement(0, s) + assert str(source) == "def func(): raise ValueError(42)" + +def XXX_test_expression_multiline(): + source = """\ +something +''' +'''""" + result = getstatement(1, source) + assert str(result) == "'''\n'''" + https://bitbucket.org/hpk42/py/changeset/2481690036ba/ changeset: 2481690036ba user: hpk42 date: 2012-10-25 11:38:25 summary: introduce an optional ASTcache passed in by exception-info printing to avoid reparsing the same module over and over affected #: 5 files diff -r 2ccc7c44763d32e794d46e12933053c07962eb52 -r 2481690036bae55d4cb613cba41490ed3839105e py/__init__.py --- a/py/__init__.py +++ b/py/__init__.py @@ -8,7 +8,7 @@ (c) Holger Krekel and others, 2004-2010 """ -__version__ = '1.4.11.dev5' +__version__ = '1.4.11.dev6' from py import _apipkg diff -r 2ccc7c44763d32e794d46e12933053c07962eb52 -r 2481690036bae55d4cb613cba41490ed3839105e py/_code/code.py --- a/py/_code/code.py +++ b/py/_code/code.py @@ -160,16 +160,28 @@ # on Jython this firstlineno can be -1 apparently return max(self.frame.code.firstlineno, 0) - def getsource(self): + def getsource(self, astcache=None): """ return failing source code. """ + # we use the passed in astcache to not reparse asttrees + # within exception info printing + from py._code.source import getstatementrange_ast source = self.frame.code.fullsource if source is None: return None + key = astnode = None + if astcache is not None: + key = self.frame.code.path + if key is not None: + astnode = astcache.get(key, None) start = self.getfirstlinesource() try: - _, end = source.getstatementrange(self.lineno) + astnode, _, end = getstatementrange_ast(self.lineno, source, + astnode=astnode) except SyntaxError: end = self.lineno + 1 + else: + if key is not None: + astcache[key] = astnode return source[start:end] source = property(getsource) @@ -391,6 +403,7 @@ self.tbfilter = tbfilter self.funcargs = funcargs self.abspath = abspath + self.astcache = {} def _getindent(self, source): # figure out indent for given source @@ -408,7 +421,7 @@ return 4 + (len(s) - len(s.lstrip())) def _getentrysource(self, entry): - source = entry.getsource() + source = entry.getsource(self.astcache) if source is not None: source = source.deindent() return source diff -r 2ccc7c44763d32e794d46e12933053c07962eb52 -r 2481690036bae55d4cb613cba41490ed3839105e py/_code/source.py --- a/py/_code/source.py +++ b/py/_code/source.py @@ -111,7 +111,8 @@ """ if not (0 <= lineno < len(self)): raise IndexError("lineno out of range") - return getstatementrange_ast(lineno, self) + ast, start, end = getstatementrange_ast(lineno, self) + return start, end def deindent(self, offset=None): """ return a new source object deindented by offset. @@ -357,10 +358,10 @@ #print "returning nodelist", l return l -def getstatementrange_ast(lineno, source): - content = "\n".join(source.lines) - #print "compiling lines", len(source.lines) - astnode = compile(content, "source", "exec", 1024) # 1024 for AST +def getstatementrange_ast(lineno, source, astnode=None): + if astnode is None: + content = "\n".join(source.lines) + astnode = compile(content, "source", "exec", 1024) # 1024 for AST start, end = get_statement_startend(lineno, getnodelist(astnode)) # we need to correct the end: # - ast-parsing strips comments @@ -375,5 +376,5 @@ end -= 1 else: break - return start, end + return astnode, start, end diff -r 2ccc7c44763d32e794d46e12933053c07962eb52 -r 2481690036bae55d4cb613cba41490ed3839105e setup.py --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ name='py', description='library with cross-python path, ini-parsing, io, code, log facilities', long_description = open('README.txt').read(), - version='1.4.11.dev5', + version='1.4.11.dev6', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff -r 2ccc7c44763d32e794d46e12933053c07962eb52 -r 2481690036bae55d4cb613cba41490ed3839105e testing/code/test_source.py --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -463,7 +463,7 @@ def getstatement(lineno, source): from py._code.source import getstatementrange_ast source = py.code.Source(source, deindent=False) - start, end = getstatementrange_ast(lineno, source) + ast, start, end = getstatementrange_ast(lineno, source) return source[start:end] def test_oneline(): Repository URL: https://bitbucket.org/hpk42/py/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. _______________________________________________ py-svn mailing list py-svn@codespeak.net http://codespeak.net/mailman/listinfo/py-svn