Repository navigation
bpo-28180: Implementation for PEP 538 #659
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
b5d125b
78c17a7
721b27f
16a4415
d283de1
f7a03fe
fe92a29
64d9d2f
384a146
b4f3a34
4d684a6
1c3a270
7626fcf
d12b412
ec4f2ea
4e6d502
ccfc83f
b173af3
501a829
d099a52
762a09b
820bfad
6a00ce6
188e780
476a781
123ba24
939ba0a
6d564c9
53bd6da
cad0669
421516f
e48a378
f62dbd8
d181b92
8cf0590
cea7970
c63d5fa
8e0e1ca
7379398
89759b5
5a56a3f
0036bea
5288662
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| # Tests the attempted automatic coercion of the C locale to a UTF-8 locale | ||
|
|
||
| import unittest | ||
| import os | ||
| import sys | ||
| import sysconfig | ||
| import shutil | ||
| import subprocess | ||
| import test.support | ||
| from test.support.script_helper import ( | ||
| run_python_until_end, | ||
| interpreter_requires_environment, | ||
| ) | ||
|
|
||
| # 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 | ||
| _C_UTF8_LOCALES = ( | ||
| # Entries: (Target locale, target category, expected env var updates) | ||
| ("C.UTF-8", "LC_ALL", "LC_ALL & LANG"), | ||
| ("C.utf8", "LC_ALL", "LC_ALL & LANG"), | ||
| ("UTF-8", "LC_CTYPE", "LC_CTYPE"), | ||
| ) | ||
|
|
||
| # There's no reliable cross-platform way of checking locale alias | ||
| # lists, so the only way of knowing which of these locales will work | ||
| # is to try them with locale.setlocale(). We do that in a subprocess | ||
| # to avoid altering the locale of the test runner. | ||
| def _set_locale_in_subprocess(locale_name, category): | ||
| cmd_fmt = "import locale; print(locale.setlocale(locale.{}, '{}'))" | ||
| cmd = cmd_fmt.format(category, locale_name) | ||
| result, py_cmd = run_python_until_end("-c", cmd, __isolated=True) | ||
| return result.rc == 0 | ||
|
|
||
| # Details of the CLI warning emitted at runtime | ||
| CLI_COERCION_WARNING_FMT = ( | ||
| "Python detected LC_CTYPE=C: {} coerced to {} (set another locale " | ||
| "or PYTHONCOERCECLOCALE=0 to disable this locale coercion behaviour)." | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does the value of the envar matter or not? It just needs to be any string value, right?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Docs bug - the warning matches the behaviour, the docs don't.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Docs fixed in 1c3a270 |
||
| ) | ||
|
|
||
| @test.support.cpython_only | ||
| @unittest.skipUnless(sysconfig.get_config_var("PY_COERCE_C_LOCALE"), | ||
| "C locale coercion disabled at build time") | ||
| class LocaleOverrideTests(unittest.TestCase): | ||
|
|
||
| @classmethod | ||
| def setUpClass(cls): | ||
| for target_locale, target_category, env_updates in _C_UTF8_LOCALES: | ||
| if _set_locale_in_subprocess(target_locale, target_category): | ||
| break | ||
| else: | ||
| raise unittest.SkipTest("No C-with-UTF-8 locale available") | ||
| cls.EXPECTED_COERCION_WARNING = CLI_COERCION_WARNING_FMT.format( | ||
| env_updates, target_locale | ||
| ) | ||
|
|
||
| def _get_child_fsencoding(self, env_vars): | ||
| """Retrieves sys.getfilesystemencoding() from a child process | ||
|
|
||
| Returns (fsencoding, stderr_lines): | ||
|
|
||
| - fsencoding: a lowercase str value with the child's fsencoding | ||
| - stderr_lines: result of calling splitlines() on the stderr output | ||
|
|
||
| The child is run in isolated mode if the current interpreter supports | ||
| that. | ||
| """ | ||
| cmd = "import sys; print(sys.getfilesystemencoding().lower())" | ||
| result, py_cmd = run_python_until_end( | ||
| "-c", cmd, | ||
| __isolated=True, | ||
| **env_vars | ||
| ) | ||
| if not result.rc == 0: | ||
| result.fail(py_cmd) | ||
| # All subprocess outputs in this test case should be pure ASCII | ||
| child_fsencoding = result.out.decode("ascii").rstrip() | ||
| child_stderr_lines = result.err.decode("ascii").rstrip().splitlines() | ||
| return child_fsencoding, child_stderr_lines | ||
|
|
||
|
|
||
| def test_C_utf8_locale(self): | ||
| # Ensure the C.UTF-8 locale is accepted entirely without complaint | ||
| base_var_dict = { | ||
| "LANG": "", | ||
| "LC_CTYPE": "", | ||
| "LC_ALL": "", | ||
| } | ||
| for env_var in ("LC_ALL", "LC_CTYPE", "LANG"): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does the order matter? If not, what about using
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in d12b412 |
||
| with self.subTest(env_var=env_var): | ||
| var_dict = base_var_dict.copy() | ||
| var_dict[env_var] = "C.UTF-8" | ||
| fsencoding, stderr_lines = self._get_child_fsencoding(var_dict) | ||
| self.assertEqual(fsencoding, "utf-8") | ||
| self.assertFalse(stderr_lines) | ||
|
|
||
|
|
||
| def _check_c_locale_coercion(self, expected_fsencoding, coerce_c_locale): | ||
| """Check the handling of the C locale for various configurations | ||
|
|
||
| Parameters: | ||
| expected_fsencoding: the encoding the child is expected to report | ||
| allow_c_locale: setting to use for PYTHONALLOWCLOCALE | ||
| None: don't set the variable at all | ||
| str: the value set in the child's environment | ||
| """ | ||
| expected_warning = [] | ||
| if coerce_c_locale != "0": | ||
| # Check C locale is coerced with a warning on stderr | ||
| expected_warning.append(self.EXPECTED_COERCION_WARNING) | ||
| base_var_dict = { | ||
| "LANG": "", | ||
| "LC_CTYPE": "", | ||
| "LC_ALL": "", | ||
| } | ||
| for env_var in ("LC_ALL", "LC_CTYPE", "LANG"): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And here...
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in d12b412 |
||
| for locale_to_set in ("", "C", "POSIX", "invalid.ascii"): | ||
| with self.subTest(env_var=env_var, | ||
| nominal_locale=locale_to_set, | ||
| PYTHONCOERCECLOCALE=coerce_c_locale): | ||
| var_dict = base_var_dict.copy() | ||
| var_dict[env_var] = locale_to_set | ||
| if coerce_c_locale is not None: | ||
| var_dict["PYTHONCOERCECLOCALE"] = coerce_c_locale | ||
| fsencoding, stderr_lines = self._get_child_fsencoding(var_dict) | ||
| self.assertEqual(fsencoding, expected_fsencoding) | ||
| self.assertEqual(stderr_lines, expected_warning) | ||
|
|
||
|
|
||
| def test_test_PYTHONCOERCECLOCALE_not_set(self): | ||
| # This should coerce to the C.UTF-8 locale by default | ||
| self._check_c_locale_coercion("utf-8", coerce_c_locale=None) | ||
|
|
||
| def test_PYTHONCOERCECLOCALE_not_zero(self): | ||
| # *Any* string other that "0" is considered "set" for our purposes | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't this contradict the documentation at the top of this PR?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Docs fixed in 1c3a270 |
||
| # and hence should result in the locale coercion being enabled | ||
| for setting in ("", "1", "true", "false"): | ||
| self._check_c_locale_coercion("utf-8", coerce_c_locale=setting) | ||
|
|
||
| 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") | ||
|
|
||
|
|
||
| # Details of the shared library warning emitted at runtime | ||
| LIBRARY_C_LOCALE_WARNING = ( | ||
| "Python runtime initialized with LC_CTYPE=C (a locale with default ASCII " | ||
| "encoding), which may cause Unicode compatibility problems. Using C.UTF-8, " | ||
| "C.utf8, or UTF-8 (if available) as alternative Unicode-compatible " | ||
| "locales is recommended.\n" | ||
| ) | ||
|
|
||
| @unittest.skipUnless(sysconfig.get_config_var("PY_WARN_ON_C_LOCALE"), | ||
| "C locale runtime warning disabled at build time") | ||
| class EmbeddingTests(unittest.TestCase): | ||
| def setUp(self): | ||
| here = os.path.abspath(__file__) | ||
| basepath = os.path.dirname(os.path.dirname(os.path.dirname(here))) | ||
| self.test_exe = exe = os.path.join(basepath, "Programs", "_testembed") | ||
| if not os.path.exists(exe): | ||
| self.skipTest("%r doesn't exist" % exe) | ||
| # This is needed otherwise we get a fatal error: | ||
| # "Py_Initialize: Unable to get the locale encoding | ||
| # LookupError: no codec search functions registered: can't find encoding" | ||
| self.oldcwd = os.getcwd() | ||
| os.chdir(basepath) | ||
|
|
||
| def tearDown(self): | ||
| os.chdir(self.oldcwd) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about using
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point - I stole the basic structure from
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in d12b412 |
||
|
|
||
| def run_embedded_interpreter(self, *args): | ||
| """Runs a test in the embedded interpreter""" | ||
| cmd = [self.test_exe] | ||
| cmd.extend(args) | ||
| p = subprocess.Popen(cmd, | ||
| stdout=subprocess.PIPE, | ||
| stderr=subprocess.PIPE, | ||
| universal_newlines=True) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it make any sense to run this without
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this particular test, I don't think so, as it's just checking that the runtime warning gets emitted when we bypass the locale coercion in the standard Python CLI, rather than doing anything with the standard streams. However, it does make sense for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So it turns out that even
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. However, now that the PEP potentially also affects the way the standard streams are configured, I expanded on the test cases to also cover that: 4e6d502 |
||
| (out, err) = p.communicate() | ||
| self.assertEqual(p.returncode, 0, | ||
| "bad returncode %d, stderr is %r" % | ||
| (p.returncode, err)) | ||
| return out, err | ||
|
|
||
| def test_library_c_locale_warning(self): | ||
| # Checks forced configuration of embedded interpreter IO streams | ||
| out, err = self.run_embedded_interpreter("c_locale_warning") | ||
| if test.support.verbose > 1: | ||
| print() | ||
| print(out) | ||
| print(err) | ||
| self.assertEqual(out, "") | ||
| self.assertEqual(err, LIBRARY_C_LOCALE_WARNING) | ||
|
|
||
| def test_main(): | ||
| test.support.run_unittest(LocaleOverrideTests, EmbeddingTests) | ||
| test.support.reap_children() | ||
|
|
||
| if __name__ == "__main__": | ||
| test_main() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I reading this right? It seems odd that setting
PYTHONCOERCECLOCALEwould disable coercing the legacy locale. The sense is exactly opposite of what I'd expect.Should the envar be named
PYTHONNOCOERCECLOCALE?Also, what if the environment variable is set to
"0"? Given the above description, that should still skip coercion.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, that's a holdover from when the setting was PYTHONALLOWCLOCALE - presumably I changed the title of the section, but then got distracted by something else before updating the body.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 1c3a270