Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b5d125b
WIP: PEP 538 reference implementation
ncoghlan Mar 5, 2017
78c17a7
Fix test case failures
ncoghlan Mar 11, 2017
721b27f
Merge remote-tracking branch 'origin/master' into pep538-coerce-c-locale
ncoghlan Mar 11, 2017
16a4415
Merge branch 'master' into pep538-coerce-c-locale
ncoghlan Mar 13, 2017
d283de1
Clarify locale coercion warnings
ncoghlan Mar 13, 2017
f7a03fe
Avoid -Wformat-security warning
ncoghlan Mar 13, 2017
fe92a29
Support running tests under 'LANG=C'
ncoghlan Mar 13, 2017
64d9d2f
Suppress locale warning for PYTHONCOERCECLOCALE=0
ncoghlan Mar 13, 2017
384a146
Add test case for library runtime warning
ncoghlan Mar 13, 2017
b4f3a34
Add C locale coercion and warning build flags
ncoghlan Mar 13, 2017
4d684a6
Always use C.UTF-8 on Android
ncoghlan Mar 13, 2017
1c3a270
Fix PYTHONCOERCECLOCALE docs
ncoghlan Mar 15, 2017
7626fcf
Use Py_SetStandardStreamEncoding instead of PYTHONIOENCODING
ncoghlan Mar 15, 2017
d12b412
Some test cleanups suggested by Barry
ncoghlan Mar 15, 2017
ec4f2ea
Use more precise name for test file
ncoghlan Mar 15, 2017
4e6d502
Check standard stream settings in locale coercion tests
ncoghlan Mar 15, 2017
ccfc83f
Use US spelling
ncoghlan Mar 15, 2017
b173af3
Helper function to query PYTHONCOERCECLOCALE
ncoghlan Mar 15, 2017
501a829
Merge remote-tracking branch 'origin/master' into pep538-coerce-c-locale
ncoghlan Mar 15, 2017
d099a52
Fix ReST markup
ncoghlan Mar 15, 2017
762a09b
Fix Py_DEBUG/Py_SetStandardStreamEncoding compatibility problem
ncoghlan Mar 15, 2017
820bfad
Restore Windows _testembed compatibility
ncoghlan Mar 17, 2017
6a00ce6
Merge remote-tracking branch 'origin/master' into pep538-coerce-c-locale
ncoghlan May 6, 2017
188e780
Update to latest version of PEP 538
ncoghlan May 6, 2017
476a781
Change locale coercion to always respect LC_ALL
ncoghlan May 9, 2017
123ba24
Merge remote-tracking branch 'origin/master' into pep538-coerce-c-locale
ncoghlan May 27, 2017
939ba0a
Don't set LANG during locale coercion
ncoghlan May 27, 2017
6d564c9
Update docs to match current behaviour
ncoghlan Jun 3, 2017
53bd6da
Address CI failure and review comments
ncoghlan Jun 3, 2017
cad0669
OK, two-use function :)
ncoghlan Jun 3, 2017
421516f
Still check for the C locale in Windows
ncoghlan Jun 3, 2017
e48a378
Check actual control flow on Appveyor
ncoghlan Jun 3, 2017
f62dbd8
Merge remote-tracking branch 'origin/master' into pep538-coerce-c-locale
ncoghlan Jun 3, 2017
d181b92
Use correct reference type in docs
ncoghlan Jun 3, 2017
8cf0590
More Appveyor debugging
ncoghlan Jun 3, 2017
cea7970
New theory regarding the Windows problem
ncoghlan Jun 3, 2017
c63d5fa
Locale coercion may inject LC_CTYPE into environment
ncoghlan Jun 3, 2017
8e0e1ca
Ensure SYSTEMROOT is set in Windows embedding tests
ncoghlan Jun 3, 2017
7379398
Don't use the default pipe encoding in test_capi
ncoghlan Jun 3, 2017
89759b5
stdin encoding ends up normalised on Windows
ncoghlan Jun 3, 2017
5a56a3f
PEP 538: Add What's New entry
ncoghlan Jun 4, 2017
0036bea
Merge remote-tracking branch 'origin/master' into pep538-coerce-c-locale
ncoghlan Jun 11, 2017
5288662
Add NEWS entry
ncoghlan Jun 11, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Update to latest version of PEP 538
- move all required logic inside the shared library
- explicitly setting one of the coercion target locales
  now also automatically enables "surrogateescape" on
  sys.stdin and sys.stdout
  • Loading branch information
ncoghlan committed May 6, 2017
commit 188e7807b6d9e49377aacbb287c074e5cabf70c5
11 changes: 7 additions & 4 deletions Doc/using/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -734,10 +734,13 @@ conflict.

If setting one of these locale categories succeeds, then the matching
environment variables will be set (both ``LC_ALL`` and ``LANG`` for the
``LC_ALL`` category, and ``LC_CTYPE`` for the ``LC_CTYPE`` category),
and (if :envvar:`PYTHONIOENCODING` is not explicitly set), the text encoding
and error handling for Python's standard streams in the current process
will be set to ``utf-8`` and ``surrogateescape`` respectively.
``LC_ALL`` category, and ``LC_CTYPE`` for the ``LC_CTYPE`` category) in
the current process environment before the Python runtime is initialized.

Configuring one of these locales (either explicitly or via the above
implicit locale coercion) will automatically set the error handler for
:data:`sys.stdin` and :data:`sys.stdout` to ``surrogateescape``. This
behavior can be overridden using :envvar:`PYTHONIOENCODING` as usual.

Availability: \*nix

Expand Down
57 changes: 47 additions & 10 deletions Lib/test/test_c_locale_coercion.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)

# In order to get the warning messages to match up as expected, the candidate
# order here must much the target locale order in Programs/python.c
# order here must much the target locale order in Python/pylifecycle.c
_C_UTF8_LOCALES = (
# Entries: (Target locale, target category, expected env var updates)
("C.UTF-8", "LC_ALL", "LC_ALL & LANG"),
Expand Down Expand Up @@ -63,9 +63,13 @@ def get_expected_details(cls, expected_fsencoding):
return dict(cls(expected_fsencoding, *stream_info)._asdict())

@staticmethod
def _replace_ascii_alias(data):
"""ASCII may be reported as ANSI_X3.4-1968, so replace it in output"""
return data.replace(b"ANSI_X3.4-1968", b"ascii")
def _handle_output_variations(data):
"""Adjust the output to handle platform specific idiosyncrasies

* Some platforms report ASCII as ANSI_X3.4-1968
* Some platforms report UTF-8 instead of utf-8
"""
return data.replace(b"ANSI_X3.4-1968", b"ascii").lower()

@classmethod
def get_child_details(cls, env_vars):
Expand All @@ -87,7 +91,7 @@ def get_child_details(cls, env_vars):
if not result.rc == 0:
result.fail(py_cmd)
# All subprocess outputs in this test case should be pure ASCII
adjusted_output = cls._replace_ascii_alias(result.out)
adjusted_output = cls._handle_output_variations(result.out)
stdout_lines = adjusted_output.decode("ascii").rstrip().splitlines()
child_encoding_details = dict(cls(*stdout_lines)._asdict())
stderr_lines = result.err.decode("ascii").rstrip().splitlines()
Expand All @@ -99,15 +103,24 @@ def get_child_details(cls, env_vars):
"C locale coercion disabled at build time")
class LocaleOverrideTests(unittest.TestCase):

available_targets = []

@classmethod
def setUpClass(cls):
first_target_locale = first_env_updates = None
available_targets = cls.available_targets
# Find the target locales available in the current system
for target_locale, target_category, env_updates in _C_UTF8_LOCALES:
if _set_locale_in_subprocess(target_locale, target_category):
break
else:
available_targets.append(target_locale)
if first_target_locale is None:
first_target_locale = target_locale
first_env_updates = env_updates
if not available_targets:
raise unittest.SkipTest("No C-with-UTF-8 locale available")
# Expect coercion to use the first available locale
cls.EXPECTED_COERCION_WARNING = CLI_COERCION_WARNING_FMT.format(
env_updates, target_locale
first_env_updates, first_target_locale
)

def _check_child_encoding_details(self,
Expand Down Expand Up @@ -141,11 +154,12 @@ def _check_c_locale_coercion(self, expected_fsencoding, coerce_c_locale):
"""

# Check for expected warning on stderr if C locale is coerced
self.maxDiff = None

expected_warning = []
if coerce_c_locale != "0":
expected_warning.append(self.EXPECTED_COERCION_WARNING)

self.maxDiff = None
base_var_dict = {
"LANG": "",
"LC_CTYPE": "",
Expand All @@ -166,7 +180,7 @@ def _check_c_locale_coercion(self, expected_fsencoding, coerce_c_locale):


def test_test_PYTHONCOERCECLOCALE_not_set(self):
# This should coerce to the C.UTF-8 locale by default
# This should coerce to the first available target locale by default
self._check_c_locale_coercion("utf-8", coerce_c_locale=None)

def test_PYTHONCOERCECLOCALE_not_zero(self):
Expand All @@ -179,6 +193,29 @@ def test_PYTHONCOERCECLOCALE_set_to_zero(self):
# The setting "0" should result in the locale coercion being disabled
self._check_c_locale_coercion("ascii", coerce_c_locale="0")

def test_external_target_locale_configuration(self):
# Explicitly setting a target locale should give the same behaviour as
# is seen when implicitly coercing to that target locale
self.maxDiff = None

expected_warning = []
expected_fsencoding = "utf-8"

base_var_dict = {
"LANG": "",
"LC_CTYPE": "",
"LC_ALL": "",
}
for env_var in base_var_dict:
for locale_to_set in self.available_targets:
with self.subTest(env_var=env_var,
configured_locale=locale_to_set):
var_dict = base_var_dict.copy()
var_dict[env_var] = locale_to_set
self._check_child_encoding_details(var_dict,
expected_fsencoding,
expected_warning)


# Details of the shared library warning emitted at runtime
LIBRARY_C_LOCALE_WARNING = (
Expand Down
114 changes: 5 additions & 109 deletions Programs/python.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ wmain(int argc, wchar_t **argv)
}
#else

/* Helpers to better handle the legacy C locale
/* Access private pylifecycle helper API to better handle the legacy C locale
*
* The legacy C locale assumes ASCII as the default text encoding, which
* causes problems not only for the CPython runtime, but also other
Expand All @@ -27,107 +27,8 @@ wmain(int argc, wchar_t **argv)
* See the documentation of the PYTHONCOERCECLOCALE setting for more details.
*
*/

#ifdef PY_COERCE_C_LOCALE
/* Access private pylifecycle API to check PYTHONCOERCECLOCALE */
extern int _Py_CLocaleCoercionIsExpected(void);

static const char *_C_LOCALE_COERCION_WARNING =
"Python detected LC_CTYPE=C: %.20s coerced to %.20s (set another locale "
"or PYTHONCOERCECLOCALE=0 to disable this locale coercion behavior).\n";

typedef struct _CandidateLocale {
const char *locale_name;
int category;
} _LocaleCoercionTarget;

static _LocaleCoercionTarget _TARGET_LOCALES[] = {
{ "C.UTF-8", LC_ALL },
{ "C.utf8", LC_ALL },
{ "UTF-8", LC_CTYPE },
{ NULL, 0 }
};

void
_coerce_default_locale_settings(const _LocaleCoercionTarget *target)
{
const char *newloc = target->locale_name;
int category = target->category;

/* Reset locale back to currently configured defaults */
setlocale(LC_ALL, "");

/* Set the relevant locale environment variables */
if (category == LC_ALL) {
const char *env_vars_updated = "LC_ALL & LANG";
if (setenv("LC_ALL", newloc, 1)) {
fprintf(stderr,
"Error setting LC_ALL, skipping C locale coercion\n");
return;
}
if (setenv("LANG", newloc, 1)) {
fprintf(stderr,
"Error setting LANG during C locale coercion\n");
env_vars_updated = "LC_ALL";
}
fprintf(stderr, _C_LOCALE_COERCION_WARNING, env_vars_updated, newloc);
} else if (category == LC_CTYPE) {
if (setenv("LC_CTYPE", newloc, 1)) {
fprintf(stderr,
"Error setting LC_CTYPE, skipping C locale coercion\n");
return;
}
fprintf(stderr, _C_LOCALE_COERCION_WARNING, "LC_CTYPE", newloc);
} else {
fprintf(stderr, "Locale coercion must target LC_ALL or LC_CTYPE\n");
return;
}

/* Set standard stream encoding if PYTHONIOENCODING is not set
*
* We avoid setting PYTHONIOENCODING, as that can confuse Python 2
* instances in subprocesses that inherit the environment (as Python
* 2 has no 'surrogateescape' error handler).
*
* If PEP 540 is also implemented, this check will be replaced with
* unconditionally setting PYTHONUTF8=1
*/
const char *io_encoding = getenv("PYTHONIOENCODING");
if ((io_encoding == NULL) || (strnlen(io_encoding, 1) == 0)) {
Py_SetStandardStreamEncoding("utf-8", "surrogateescape");
}

/* Reconfigure with the overridden environment variables */
setlocale(LC_ALL, "");
}

void
_handle_legacy_c_locale(void)
{
/* We ignore the Python -E and -I flags here, as we need to sort out
* the locale settings *before* we try to do anything with the command
* line arguments. For cross-platform debugging purposes, we also need
* to give end users a way to force even scripts that are otherwise
* isolated from their environment to use the legacy ASCII-centric C
* locale.
*/
if (_Py_CLocaleCoercionIsExpected()) {
/* PYTHONCOERCECLOCALE is not set, or is not set to exactly "0" */
const _LocaleCoercionTarget *target = NULL;
for (target = _TARGET_LOCALES; target->locale_name; target++) {
const char *reconfigured_locale = setlocale(target->category,
target->locale_name);
if (reconfigured_locale != NULL) {
/* Successfully configured locale, so make it the default */
_coerce_default_locale_settings(target);
return;
}
}

}
/* No C locale warning here, as Py_Initialize will emit one later */
}
#endif
extern int _Py_LegacyLocaleDetected(void);
extern void _Py_CoerceLegacyLocale(void);

int
main(int argc, char **argv)
Expand Down Expand Up @@ -177,14 +78,9 @@ main(int argc, char **argv)
setlocale(LC_ALL, "");
#endif

#ifdef PY_COERCE_C_LOCALE
/* When the LC_CTYPE category still claims to be using the C locale,
assume configuration error and try for a UTF-8 based locale instead */
const char *ctype_loc = setlocale(LC_CTYPE, NULL);
if (ctype_loc != NULL && strcmp(ctype_loc, "C") == 0) {
_handle_legacy_c_locale();
if (_Py_LegacyLocaleDetected()) {
_Py_CoerceLegacyLocale();
}
#endif

/* Convert from char to wchar_t based on the locale settings */
for (i = 0; i < argc; i++) {
Expand Down
Loading