Skip to content

Commit

Permalink
Speed up chunk writing in the PyxCodeWriter since we started using it…
Browse files Browse the repository at this point in the history
… quite a bit.
  • Loading branch information
scoder committed Aug 25, 2024
1 parent ca35890 commit 721d63f
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 14 deletions.
3 changes: 0 additions & 3 deletions Cython/Compiler/Code.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,3 @@ cdef class PyxCodeWriter:
cdef Py_ssize_t level
cdef Py_ssize_t original_level
cdef dict _insertion_points

@cython.final
cdef int _putln(self, line) except -1
83 changes: 72 additions & 11 deletions Cython/Compiler/Code.py
Original file line number Diff line number Diff line change
Expand Up @@ -3086,22 +3086,23 @@ def getvalue(self):
return result

def putln(self, line, context=None):
context = context or self.context
if context:
if context is None:
if self.context is not None:
context = self.context
if context is not None:
line = sub_tempita(line, context)
self._putln(line)

def _putln(self, line):
self.buffer.write(f"{self.level * ' '}{line}\n")
# Avoid indenting empty lines.
self.buffer.write(f"{self.level * ' '}{line}\n" if line else "\n")

def put_chunk(self, chunk, context=None):
context = context or self.context
if context:
if context is None:
if self.context is not None:
context = self.context
if context is not None:
chunk = sub_tempita(chunk, context)

chunk = textwrap.dedent(chunk)
for line in chunk.splitlines():
self._putln(line)
chunk = _indent_chunk(chunk, self.level * 4)
self.buffer.write(chunk)

def insertion_point(self):
return type(self)(self.buffer.insertion_point(), self.level, self.context)
Expand All @@ -3119,6 +3120,66 @@ def __getitem__(self, name):
return self._insertion_points[name]


@cython.final
@cython.ccall
def _indent_chunk(chunk: str, indentation_length: cython.int) -> str:
"""Normalise leading space to the intended indentation and strip empty lines.
"""
assert '\t' not in chunk
lines = chunk.splitlines(keepends=True)
if not lines:
return chunk
last_line = lines[-1].rstrip(' ')
if last_line:
lines[-1] = last_line
else:
del lines[-1]
if not lines:
return '\n'

# Count minimal (non-empty) indentation and strip empty lines.
min_indentation: cython.int = len(chunk) + 1
line_indentation: cython.int
line: str
i: cython.int
for i, line in enumerate(lines):
line_indentation = _count_indentation(line)
if line_indentation + 1 == len(line):
lines[i] = '\n'
elif line_indentation < min_indentation:
min_indentation = line_indentation

if min_indentation > len(chunk):
# All empty lines.
min_indentation = 0

if min_indentation < indentation_length:
add_indent = ' ' * (indentation_length - min_indentation)
lines = [
add_indent + line if line != '\n' else '\n'
for line in lines
]
elif min_indentation > indentation_length:
start: cython.int = min_indentation - indentation_length
lines = [
line[start:] if line != '\n' else '\n'
for line in lines
]

return ''.join(lines)


@cython.exceptval(-1)
@cython.cfunc
def _count_indentation(s: str) -> cython.int:
i: cython.int = 0
ch: cython.Py_UCS4
for i, ch in enumerate(s):
if ch != ' ':
break
return i


class ClosureTempAllocator:
def __init__(self, klass):
self.klass = klass
Expand Down
86 changes: 86 additions & 0 deletions Cython/Compiler/Tests/TestCode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import textwrap
from unittest import TestCase

from ..Code import _indent_chunk

class TestIndent(TestCase):
def _test_indentations(self, chunk, expected):
for indentation in range(16):
expected_indented = textwrap.indent(expected, ' ' * indentation)
for line in expected_indented.splitlines():
# Validate before the comparison that empty lines got stripped also by textwrap.indent().
self.assertTrue(line == '' or line.strip(), repr(line))

with self.subTest(indentation=indentation):
result = _indent_chunk(chunk, indentation_length=indentation)
self.assertEqual(expected_indented, result)

def test_indent_empty(self):
self._test_indentations('', '')

def test_indent_empty_lines(self):
self._test_indentations('\n', '\n')
self._test_indentations('\n'*2, '\n'*2)
self._test_indentations('\n'*3, '\n'*3)
self._test_indentations(' \n'*2, '\n'*2)
self._test_indentations('\n \n \n \n', '\n'*4)

def test_indent_one_line(self):
self._test_indentations('abc', 'abc')

def test_indent_chunk(self):
chunk = """
x = 1
if x == 2:
print("False")
else:
print("True")
"""
expected = """
x = 1
if x == 2:
print("False")
else:
print("True")
"""
self._test_indentations(chunk, expected)

def test_indent_empty_line(self):
chunk = """
x = 1
if x == 2:
print("False")
else:
print("True")
"""
expected = """
x = 1
if x == 2:
print("False")
else:
print("True")
"""
self._test_indentations(chunk, expected)

def test_indent_empty_line_unclean(self):
lines = """
x = 1
if x == 2:
print("False")
else:
print("True")
""".splitlines(keepends=True)
lines[2] = ' \n'
chunk = ''.join(lines)
expected = """
x = 1
if x == 2:
print("False")
else:
print("True")
"""
self._test_indentations(chunk, expected)

0 comments on commit 721d63f

Please sign in to comment.