You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README-Unicode.md
+46-15
Original file line number
Diff line number
Diff line change
@@ -1,40 +1,64 @@
1
-
Python3 Unicode migration in the XCP package
2
-
============================================
1
+
# Python3 Unicode migration in the XCP package
2
+
3
+
## Problem
4
+
5
+
Python3.6 on XS8 does not have an all-encompassing default UTF-8 mode for I/O.
6
+
7
+
Newer Python versions have an UTF-8 mode that they even enable by default.
8
+
Python3.6 only enabled UTF-8 for I/O when an UTF-8 locale is used.
9
+
See below for more background info on the UTF-8 mode.
10
+
11
+
For situations where UTF-8 enabled, we have to specify UTF-8 explicitly.
12
+
13
+
Such sitation happens when LANG or LC_* variables are not set for UTF-8.
14
+
XAPI plugins like auto-cert-kit find themself in this situation.
15
+
16
+
Example:
17
+
For reading UTF-8 files like the `pciids` file, add `encoding="utf-8"`.
18
+
This applies especailly to `open()` and `Popen()` when files my contain UTF-8.
19
+
20
+
This also applies when en/decoding to/form `urllib` which uses bytes.
21
+
`urllib` has to use bytes as HTTP data can of course also be binary, e.g. compressed.
22
+
23
+
## Migrating `subprocess.Popen()`
3
24
4
-
Migrating `subprocess.Popen()`
5
-
------------------------------
6
25
With Python3, the `stdin`, `stdout` and `stderr` pipes for `Popen()` default to `bytes`(binary mode). Binary mode is much safer because it foregoes the encode/decode.
7
26
8
27
The for working with strings, existing users need to either enable text mode (when safe, it will attempt to decode and encode!) or be able to use bytes instead.
9
28
10
29
For cases where the data is guaranteed to be pure ASCII, such as when resting the `proc.stdout` of `lspci -nm`, it is sufficient to use:
This is possible because `universal_newlines=True`is accepted by Python2 and Python3.
15
36
On Python3, it also enables text mode for`subprocess.PIPE` streams (newline conversion
16
37
not needed, but text mode is needed)
17
38
18
-
Migrating `builtins.open()`
19
-
---------------------------
39
+
## Migrating `builtins.open()`
40
+
20
41
On Python3, `builtins.open()` can be used in a number of modes:
42
+
21
43
- Binary mode (when `"b"`isin`mode`): `read()`and`write()` use `bytes`.
22
44
- Text mode (Python3 default up to Python 3.6), when UTF-8 character encoding isnotset by the locale
23
-
-UTF-8 mode (default since Python 3.7): https://peps.python.org/pep-0540/
45
+
-UTF-8 mode (default since Python 3.7): <https://peps.python.org/pep-0540/>
24
46
25
47
When no Unicode locale in force, like inXAPI plugins, Python3 will be in text mode orUTF-8 (since Python 3.7, but existing XSis on 3.6):
26
48
27
-
* By default, `read()` on files `open()`ed without selecting binary mode attempts
49
+
- By default, `read()` on files `open()`ed without selecting binary mode attempts
28
50
to decode the data into the Python3 Unicode string type.
29
51
This fails for binary data.
30
52
Any `ord() >=128`, when no UTF-8 locale is active With Python 3.6, triggers `UnicodeDecodeError`.
31
53
32
-
* Thus, all`open()` calls which might open binary files have to be converted to binary
54
+
- Thus, all`open()` calls which might open binary files have to be converted to binary
33
55
orUTF-8 mode unless the caller is sure he is opening an ASCIIfile.
34
56
But even then, enabling an error handler to handle decoding errors is recommended:
57
+
35
58
```py
36
59
open(filename, errors="replace")
37
60
```
61
+
38
62
But neither `errors=` nor `encoding=`is accepted by Python2, so a wrapper is likely best.
39
63
40
64
### Binary mode
@@ -43,21 +67,18 @@ When decoding bytes to strings is not needed, binary mode can be great because i
43
67
44
68
However, when strings need to returned from the library, something like `bytes.decode(errors="ignore")` to get strings is needed.
45
69
46
-
### Text mode
47
-
48
-
Text mode using the `ascii` codec should be only used when it is ensured that the data can only consist of ASCII characters (0-127). Sadly, it is the default in Python 3.6 when the Python interpreter was not started using an UTF-8 locale for the LC_CTYPE locale category (set by LC_ALL, LC_CTYPE, LANG environment variables, overriding each other in that order)
49
-
50
70
### UTF-8 mode
51
71
52
72
Most if the time, the `UTF-8` codec should be used since even simple text files which are even documented to contain only ASCII characters like `"/usr/share/hwdata/pci.ids"`in fact __do__ contain UTF-8 characters.
53
73
54
74
Some files or some output data from commands even contains legacy `ISO-8859-1` chars, and even the `UTF-8` codec would raise`UnicodeDecodeError`for these.
55
75
When this is known to be the case, `encoding="iso-8859-1` could be tried (not tested yet).
56
76
57
-
### Problems
77
+
### Problems
58
78
59
79
With the locale set to C (XAPI plugins have that), Python's default mode changes
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc2 in position 97850: ordinal not in range(128)
80
103
3.7:
81
104
```
105
+
82
106
This error means that the `'ascii' codec` cannot handle inputord() >=128, andas some Video cards use `²` to reference their power, the `ascii` codec chokes on them.
83
107
84
108
It means `xcp.pci.PCIIds()` cannot use `open("/usr/share/hwdata/pci.ids").read()`.
85
109
86
110
While Python 3.7and newer use UTF-8 mode by default, it does notset up an error handler for`UnicodeDecodeError`.
87
111
88
112
As it happens, some older tools output ISO-8859-1 characters hard-coded and these aren't valid UTF-8 sequences, and even newer Python versions need error handlers to not fail:
113
+
89
114
```py
90
115
echo -e "\0262"# ISO-8859-1 for: "²"
91
116
python3 -c 'open(".text").read()'
@@ -133,6 +158,7 @@ tests/test_bootloader.py line 38 in TestLinuxBootloader.setUp()
133
158
tests/test_pci.py line 96in TestPCIIds.test_videoclass_by_mock_calls()
134
159
tests/test_pci.py line 110in TestPCIIds.mock_lspci_using_open_testfile()
135
160
```
161
+
136
162
Of course, `xcp/net/ifrename` won't be affected but it would be good to fix the
137
163
warning for them as well in an intelligent way. See the proposal for that below.
138
164
@@ -141,6 +167,7 @@ arguments we need to pass to ensure that all users of open() will work, we need
141
167
to make passing the arguments conditional on Python >=3.
142
168
143
169
1. Overriding `open()`, while technically working would not only affect xcp.python but the entire program:
170
+
144
171
```py
145
172
if sys.version_info >= (3, 0):
146
173
original_open = __builtins__["open"]
@@ -152,7 +179,9 @@ to make passing the arguments conditional on Python >= 3.
152
179
return original_open(*args, **kwargs)
153
180
__builtins__["open"] = uopen
154
181
```
182
+
155
183
2. This is sufficient but isnot very nice:
184
+
156
185
```py
157
186
# xcp/utf8mode.py
158
187
if sys.version_info >= (3, 0):
@@ -165,9 +194,11 @@ to make passing the arguments conditional on Python >= 3.
165
194
-open(filename)
166
195
+open(filename, **open_utf8args)
167
196
```
197
+
168
198
But, `pylint` will still warn about these lines, so I propose:
169
199
170
200
3. Instead, use a wrapper function, which will also silence the `pylint` warnings at the locations which have been changed to use it:
201
+
171
202
```py
172
203
# xcp/utf8mode.py
173
204
if sys.version_info >= (3, 0):
@@ -189,4 +220,4 @@ Using the 3rd option, the `pylint` warnings for the changed locations
189
220
explicitly disabling them.
190
221
191
222
PS: Since utf8open() still returns a context-manager, `withopen(...) as f:`
0 commit comments