Skip to content

Commit 6050835

Browse files
committedSep 13, 2015
Merge pull request #2071 from getnikola/threadsafe-localeborg
Making LocaleBorg thread-safe.
2 parents c370d97 + dafa77f commit 6050835

File tree

2 files changed

+78
-49
lines changed

2 files changed

+78
-49
lines changed
 

‎nikola/utils.py

+77-48
Original file line numberDiff line numberDiff line change
@@ -1074,21 +1074,35 @@ def initialize(cls, locales, initial_lang):
10741074
encodings[lang] = encoding
10751075

10761076
cls.encodings = encodings
1077-
cls.__shared_state['current_lang'] = initial_lang
1077+
cls.__initial_lang = initial_lang
10781078
cls.initialized = True
10791079

1080+
def __get_shared_state(self):
1081+
if not self.initialized:
1082+
raise LocaleBorgUninitializedException()
1083+
shared_state = getattr(self.__thread_local, 'shared_state', None)
1084+
if shared_state is None:
1085+
shared_state = {'current_lang': self.__initial_lang}
1086+
self.__thread_local.shared_state = shared_state
1087+
return shared_state
1088+
10801089
@classmethod
10811090
def reset(cls):
10821091
"""Reset LocaleBorg.
10831092
10841093
Used in testing to prevent leaking state between tests.
10851094
"""
1095+
import threading
1096+
cls.__thread_local = threading.local()
1097+
cls.__thread_lock = threading.Lock()
1098+
10861099
cls.locales = {}
10871100
cls.encodings = {}
1088-
cls.__shared_state = {'current_lang': None}
10891101
cls.initialized = False
10901102
cls.month_name_handlers = []
10911103
cls.formatted_date_handlers = []
1104+
cls.thread_local = None
1105+
cls.thread_lock = None
10921106

10931107
@classmethod
10941108
def add_handler(cls, month_name_handler=None, formatted_date_handler=None):
@@ -1115,7 +1129,16 @@ def __init__(self):
11151129
"""Initialize."""
11161130
if not self.initialized:
11171131
raise LocaleBorgUninitializedException()
1118-
self.__dict__ = self.__shared_state
1132+
1133+
@property
1134+
def current_lang(self):
1135+
"""Return the current language."""
1136+
return self.__get_shared_state()['current_lang']
1137+
1138+
def __set_locale(self, lang):
1139+
"""Set the locale for language lang without updating current_lang."""
1140+
locale_n = self.locales[lang]
1141+
locale.setlocale(locale.LC_ALL, locale_n)
11191142

11201143
def set_locale(self, lang):
11211144
"""Set the locale for language lang, returns an empty string.
@@ -1124,58 +1147,64 @@ def set_locale(self, lang):
11241147
in windows that cannot be guaranted.
11251148
In either case, the locale encoding is available in cls.encodings[lang]
11261149
"""
1127-
# intentional non try-except: templates must ask locales with a lang,
1128-
# let the code explode here and not hide the point of failure
1129-
# Also, not guarded with an if lang==current_lang because calendar may
1130-
# put that out of sync
1131-
locale_n = self.locales[lang]
1132-
self.__shared_state['current_lang'] = lang
1133-
locale.setlocale(locale.LC_ALL, locale_n)
1134-
return ''
1150+
with self.__thread_lock:
1151+
# intentional non try-except: templates must ask locales with a lang,
1152+
# let the code explode here and not hide the point of failure
1153+
# Also, not guarded with an if lang==current_lang because calendar may
1154+
# put that out of sync
1155+
self.__set_locale(lang)
1156+
self.__get_shared_state()['current_lang'] = lang
1157+
return ''
11351158

11361159
def get_month_name(self, month_no, lang):
11371160
"""Return localized month name in an unicode string."""
1138-
for handler in self.month_name_handlers:
1139-
res = handler(month_no, lang)
1140-
if res is not None:
1141-
return res
1142-
if sys.version_info[0] == 3: # Python 3
1143-
with calendar.different_locale(self.locales[lang]):
1144-
s = calendar.month_name[month_no]
1145-
# for py3 s is unicode
1146-
else: # Python 2
1147-
with calendar.TimeEncoding(self.locales[lang]):
1148-
s = calendar.month_name[month_no]
1149-
enc = self.encodings[lang]
1150-
if not enc:
1151-
enc = 'UTF-8'
1152-
1153-
s = s.decode(enc)
1154-
# paranoid about calendar ending in the wrong locale (windows)
1155-
self.set_locale(self.current_lang)
1156-
return s
1161+
# For thread-safety
1162+
with self.__thread_lock:
1163+
for handler in self.month_name_handlers:
1164+
res = handler(month_no, lang)
1165+
if res is not None:
1166+
return res
1167+
if sys.version_info[0] == 3: # Python 3
1168+
with calendar.different_locale(self.locales[lang]):
1169+
s = calendar.month_name[month_no]
1170+
# for py3 s is unicode
1171+
else: # Python 2
1172+
with calendar.TimeEncoding(self.locales[lang]):
1173+
s = calendar.month_name[month_no]
1174+
enc = self.encodings[lang]
1175+
if not enc:
1176+
enc = 'UTF-8'
1177+
1178+
s = s.decode(enc)
1179+
# paranoid about calendar ending in the wrong locale (windows)
1180+
self.__set_locale(self.current_lang)
1181+
return s
11571182

11581183
def formatted_date(self, date_format, date):
11591184
"""Return the formatted date as unicode."""
1160-
fmt_date = None
1161-
# First check handlers
1162-
for handler in self.formatted_date_handlers:
1163-
fmt_date = handler(date_format, date, self.__shared_state['current_lang'])
1164-
if fmt_date is not None:
1165-
break
1166-
# If no handler was able to format the date, ask Python
1167-
if fmt_date is None:
1168-
if date_format == 'webiso':
1169-
# Formatted after RFC 3339 (web ISO 8501 profile) with Zulu
1170-
# zone desgignator for times in UTC and no microsecond precision.
1171-
fmt_date = date.replace(microsecond=0).isoformat().replace('+00:00', 'Z')
1172-
else:
1173-
fmt_date = date.strftime(date_format)
1185+
with self.__thread_lock:
1186+
current_lang = self.current_lang
1187+
# For thread-safety
1188+
self.__set_locale(current_lang)
1189+
fmt_date = None
1190+
# First check handlers
1191+
for handler in self.formatted_date_handlers:
1192+
fmt_date = handler(date_format, date, current_lang)
1193+
if fmt_date is not None:
1194+
break
1195+
# If no handler was able to format the date, ask Python
1196+
if fmt_date is None:
1197+
if date_format == 'webiso':
1198+
# Formatted after RFC 3339 (web ISO 8501 profile) with Zulu
1199+
# zone desgignator for times in UTC and no microsecond precision.
1200+
fmt_date = date.replace(microsecond=0).isoformat().replace('+00:00', 'Z')
1201+
else:
1202+
fmt_date = date.strftime(date_format)
11741203

1175-
# Issue #383, this changes from py2 to py3
1176-
if isinstance(fmt_date, bytes_str):
1177-
fmt_date = fmt_date.decode('utf8')
1178-
return fmt_date
1204+
# Issue #383, this changes from py2 to py3
1205+
if isinstance(fmt_date, bytes_str):
1206+
fmt_date = fmt_date.decode('utf8')
1207+
return fmt_date
11791208

11801209

11811210
class ExtendedRSS2(rss.RSS2):

‎tests/test_locale.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,8 @@ def test_services_ensure_initialization(self):
214214
def test_services_reject_dumb_wrong_call(self):
215215
lang_11, loc_11 = LocaleSupportInTesting.langlocales['default']
216216
nikola.utils.LocaleBorg.reset()
217+
self.assertRaises(Exception, nikola.utils.LocaleBorg)
217218
self.assertRaises(Exception, nikola.utils.LocaleBorg.set_locale, lang_11)
218-
self.assertRaises(Exception, getattr, nikola.utils.LocaleBorg, 'current_lang')
219219

220220
def test_set_locale_raises_on_invalid_lang(self):
221221
lang_11, loc_11 = LocaleSupportInTesting.langlocales['default']

0 commit comments

Comments
 (0)
Please sign in to comment.