Skip to content

Commit

Permalink
Use thread-local storage for the global Errors state to allow threade…
Browse files Browse the repository at this point in the history
…d builds. (GH-4507)

Distutils uses threading internally.

Also silence some warnings about redefined classes and function signatures when setting up the builtin scope. This is at most a second-best solution since we may not notice legitimate bugs on our side this way. Better make sure we have good test coverage of builtins and related optimisations.

Closes #4503
  • Loading branch information
scoder committed Dec 20, 2021
1 parent c6f5c5d commit f372c5a
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 54 deletions.
1 change: 1 addition & 0 deletions Cython/Compiler/Builtin.py
Expand Up @@ -420,6 +420,7 @@ def init_builtin_structs():


def init_builtins():
#Errors.init_thread() # hopefully not needed - we should not emit warnings ourselves
init_builtin_structs()
init_builtin_types()
init_builtin_funcs()
Expand Down
82 changes: 50 additions & 32 deletions Cython/Compiler/Errors.py
Expand Up @@ -12,6 +12,13 @@
import sys
from contextlib import contextmanager

try:
from threading import local as _threadlocal
except ImportError:
class _threadlocal(object): pass

threadlocal = _threadlocal()

from ..Utils import open_new_file
from . import DebugFlags
from . import Options
Expand Down Expand Up @@ -120,35 +127,29 @@ class NoElementTreeInstalledException(PyrexError):
implementation was found
"""

listing_file = None
num_errors = 0
echo_file = None

def open_listing_file(path, echo_to_stderr = 1):
def open_listing_file(path, echo_to_stderr=True):
# Begin a new error listing. If path is None, no file
# is opened, the error counter is just reset.
global listing_file, num_errors, echo_file
if path is not None:
listing_file = open_new_file(path)
threadlocal.cython_errors_listing_file = open_new_file(path)
else:
listing_file = None
threadlocal.cython_errors_listing_file = None
if echo_to_stderr:
echo_file = sys.stderr
threadlocal.cython_errors_echo_file = sys.stderr
else:
echo_file = None
num_errors = 0
threadlocal.cython_errors_echo_file = None
threadlocal.cython_errors_count = 0

def close_listing_file():
global listing_file
if listing_file:
listing_file.close()
listing_file = None
if threadlocal.cython_errors_listing_file:
threadlocal.cython_errors_listing_file.close()
threadlocal.cython_errors_listing_file = None

def report_error(err, use_stack=True):
error_stack = threadlocal.cython_errors_stack
if error_stack and use_stack:
error_stack[-1].append(err)
else:
global num_errors
# See Main.py for why dual reporting occurs. Quick fix for now.
if err.reported: return
err.reported = True
Expand All @@ -157,15 +158,17 @@ def report_error(err, use_stack=True):
# Python <= 2.5 does this for non-ASCII Unicode exceptions
line = format_error(getattr(err, 'message_only', "[unprintable exception message]"),
getattr(err, 'position', None)) + u'\n'
listing_file = threadlocal.cython_errors_listing_file
if listing_file:
try: listing_file.write(line)
except UnicodeEncodeError:
listing_file.write(line.encode('ASCII', 'replace'))
echo_file = threadlocal.cython_errors_echo_file
if echo_file:
try: echo_file.write(line)
except UnicodeEncodeError:
echo_file.write(line.encode('ASCII', 'replace'))
num_errors += 1
threadlocal.cython_errors_count += 1
if Options.fast_fail:
raise AbortError("fatal errors")

Expand Down Expand Up @@ -193,8 +196,10 @@ def message(position, message, level=1):
return
warn = CompileWarning(position, message)
line = u"note: %s\n" % warn
listing_file = threadlocal.cython_errors_listing_file
if listing_file:
_write_file_encode(listing_file, line)
echo_file = threadlocal.cython_errors_echo_file
if echo_file:
_write_file_encode(echo_file, line)
return warn
Expand All @@ -207,62 +212,75 @@ def warning(position, message, level=0):
return error(position, message)
warn = CompileWarning(position, message)
line = u"warning: %s\n" % warn
listing_file = threadlocal.cython_errors_listing_file
if listing_file:
_write_file_encode(listing_file, line)
echo_file = threadlocal.cython_errors_echo_file
if echo_file:
_write_file_encode(echo_file, line)
return warn


_warn_once_seen = {}
def warn_once(position, message, level=0):
if level < LEVEL or message in _warn_once_seen:
if level < LEVEL:
return
warn_once_seen = threadlocal.cython_errors_warn_once_seen
if message in warn_once_seen:
return
warn = CompileWarning(position, message)
line = u"warning: %s\n" % warn
listing_file = threadlocal.cython_errors_listing_file
if listing_file:
_write_file_encode(listing_file, line)
echo_file = threadlocal.cython_errors_echo_file
if echo_file:
_write_file_encode(echo_file, line)
_warn_once_seen[message] = True
warn_once_seen[message] = True
return warn


# These functions can be used to momentarily suppress errors.

error_stack = []


def hold_errors():
error_stack.append([])
errors = []
threadlocal.cython_errors_stack.append(errors)
return errors


def release_errors(ignore=False):
held_errors = error_stack.pop()
held_errors = threadlocal.cython_errors_stack.pop()
if not ignore:
for err in held_errors:
report_error(err)


def held_errors():
return error_stack[-1]
return threadlocal.cython_errors_stack[-1]


# same as context manager:

@contextmanager
def local_errors(ignore=False):
errors = []
error_stack.append(errors)
errors = hold_errors()
try:
yield errors
finally:
release_errors(ignore=ignore)


# this module needs a redesign to support parallel cythonisation, but
# for now, the following works at least in sequential compiler runs
# Keep all global state in thread local storage to support parallel cythonisation in distutils.

def init_thread():
threadlocal.cython_errors_count = 0
threadlocal.cython_errors_listing_file = None
threadlocal.cython_errors_echo_file = None
threadlocal.cython_errors_warn_once_seen = set()
threadlocal.cython_errors_stack = []

def reset():
_warn_once_seen.clear()
del error_stack[:]
threadlocal.cython_errors_warn_once_seen.clear()
del threadlocal.cython_errors_stack[:]

def get_errors_count():
return threadlocal.cython_errors_count
4 changes: 2 additions & 2 deletions Cython/Compiler/FusedNode.py
Expand Up @@ -276,12 +276,12 @@ def replace_fused_typechecks(self, copied_node):
Returns whether an error was issued and whether we should stop in
in order to prevent a flood of errors.
"""
num_errors = Errors.num_errors
num_errors = Errors.get_errors_count()
transform = ParseTreeTransforms.ReplaceFusedTypeChecks(
copied_node.local_scope)
transform(copied_node)

if Errors.num_errors > num_errors:
if Errors.get_errors_count() > num_errors:
return False

return True
Expand Down
11 changes: 5 additions & 6 deletions Cython/Compiler/Main.py
Expand Up @@ -337,7 +337,7 @@ def parse(self, source_desc, scope, pxd, full_module_name):
source_filename = source_desc.filename
scope.cpp = self.cpp
# Parse the given source file and return a parse tree.
num_errors = Errors.num_errors
num_errors = Errors.get_errors_count()
try:
with Utils.open_source_file(source_filename) as f:
from . import Parsing
Expand All @@ -356,7 +356,7 @@ def parse(self, source_desc, scope, pxd, full_module_name):
#traceback.print_exc()
raise self._report_decode_error(source_desc, e)

if Errors.num_errors > num_errors:
if Errors.get_errors_count() > num_errors:
raise CompileError()
return tree

Expand Down Expand Up @@ -396,20 +396,19 @@ def extract_module_name(self, path, options):
return ".".join(names)

def setup_errors(self, options, result):
Errors.reset() # clear any remaining error state
Errors.init_thread()
if options.use_listing_file:
path = result.listing_file = Utils.replace_suffix(result.main_source_file, ".lis")
else:
path = None
Errors.open_listing_file(path=path,
echo_to_stderr=options.errors_to_stderr)
Errors.open_listing_file(path=path, echo_to_stderr=options.errors_to_stderr)

def teardown_errors(self, err, options, result):
source_desc = result.compilation_source.source_desc
if not isinstance(source_desc, FileSourceDescriptor):
raise RuntimeError("Only file sources for code supported")
Errors.close_listing_file()
result.num_errors = Errors.num_errors
result.num_errors = Errors.get_errors_count()
if result.num_errors > 0:
err = True
if err and result.c_file:
Expand Down
4 changes: 2 additions & 2 deletions Cython/Compiler/Pipeline.py
Expand Up @@ -19,7 +19,7 @@ def dumptree(t):

def abort_on_errors(node):
# Stop the pipeline if there are any errors.
if Errors.num_errors != 0:
if Errors.get_errors_count() != 0:
raise AbortError("pipeline break")
return node

Expand Down Expand Up @@ -378,7 +378,7 @@ def run(phase, data):
error = err
except InternalError as err:
# Only raise if there was not an earlier error
if Errors.num_errors == 0:
if Errors.get_errors_count() == 0:
raise
error = err
except AbortError as err:
Expand Down
7 changes: 4 additions & 3 deletions Cython/Compiler/Symtab.py
Expand Up @@ -485,7 +485,7 @@ def declare(self, name, cname, type, pos, visibility, shadow = 0, is_type = 0, c
warning(pos, "'%s' is a reserved name in C." % cname, -1)

entries = self.entries
if name and name in entries and not shadow:
if name and name in entries and not shadow and not self.is_builtin_scope:
old_entry = entries[name]

# Reject redeclared C++ functions only if they have the same type signature.
Expand Down Expand Up @@ -831,10 +831,10 @@ def declare_cfunction(self, name, type, pos,
entry.type = entry.type.with_with_gil(type.with_gil)
else:
if visibility == 'extern' and entry.visibility == 'extern':
can_override = False
can_override = self.is_builtin_scope
if self.is_cpp():
can_override = True
elif cname:
elif cname and not can_override:
# if all alternatives have different cnames,
# it's safe to allow signature overrides
for alt_entry in entry.all_alternatives():
Expand Down Expand Up @@ -1176,6 +1176,7 @@ def declare_builtin_type(self, name, cname, utility_code = None, objstruct_cname
def builtin_scope(self):
return self

# FIXME: remove redundancy with Builtin.builtin_types_table
builtin_entries = {

"type": ["((PyObject*)&PyType_Type)", py_object_type],
Expand Down
7 changes: 2 additions & 5 deletions Cython/TestUtils.py
Expand Up @@ -51,13 +51,10 @@ def treetypes(root):
class CythonTest(unittest.TestCase):

def setUp(self):
self.listing_file = Errors.listing_file
self.echo_file = Errors.echo_file
Errors.listing_file = Errors.echo_file = None
Errors.init_thread()

def tearDown(self):
Errors.listing_file = self.listing_file
Errors.echo_file = self.echo_file
Errors.init_thread()

def assertLines(self, expected, result):
"Checks that the given strings or lists of strings are equal line by line"
Expand Down
7 changes: 3 additions & 4 deletions tests/run/test_fstring.pyx
Expand Up @@ -17,7 +17,7 @@ if IS_PY2:

from Cython.Build.Inline import cython_inline
from Cython.TestUtils import CythonTest
from Cython.Compiler.Errors import CompileError, hold_errors, release_errors, error_stack, held_errors
from Cython.Compiler.Errors import CompileError, hold_errors, init_thread, held_errors

def cy_eval(s, **kwargs):
return cython_inline('return ' + s, force=True, **kwargs)
Expand All @@ -43,7 +43,7 @@ class TestCase(CythonTest):
else:
assert held_errors(), "Invalid Cython code failed to raise SyntaxError: %r" % str
finally:
release_errors(ignore=True)
init_thread() # reset error status
else:
try:
cython_inline(str, quiet=True)
Expand All @@ -52,8 +52,7 @@ class TestCase(CythonTest):
else:
assert False, "Invalid Cython code failed to raise %s: %r" % (exception_type, str)
finally:
if error_stack:
release_errors(ignore=True)
init_thread() # reset error status

if IS_PY2:
def assertEqual(self, first, second, msg=None):
Expand Down

0 comments on commit f372c5a

Please sign in to comment.