summaryrefslogtreecommitdiff
blob: bc65ac6152f92507044f1e856d0e395948c7307c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
============
Porting tips
============

.. highlight:: python

This section highlights some of the known incompatible changes made
in Python that could break Python scripts and modules that used to work
in prior versions.  The sections are split into retroactive changes made
to all Python releases, and information specific to every Python branch
(compared to the previous one).

This guide is by no means considered complete.  If you can think
of other problems you've hit while porting your packages, please let me
know and I will update it.


Retroactive changes
===================

bpo43882_: urlsplit now strips LF, CR and HT characters
-------------------------------------------------------
Changed in: 2.7.18_p9, 3.6.13_p3, 3.7.10_p3, 3.8.9_p2, 3.9.4_p1

Historically, various urllib.parse_ methods have passed special
characters such as LF, CR and HT through into the split URL components.
This could have resulted in various exploits if Python programs did not
validate the resulting components and used them verbatim.

bpo43882_ attempted to address the issue by making urllib.parse_ strip
the three aforementioned characters from the output of its functions.
This fixed one class of potential issues but at the same time opened
another can of worms.  For example, URL validators that used to check
for dangerous special characters in the split URL components stopped
working correctly.  In the best case, the URL were now sanitized instead
of being rejected.  In the worst, the original unparsed URL with
dangerous characters started being passed through.  See e.g. `django
PR#14349`_ for an example of impact and a fix.

Behavior before::

    >>> urllib.parse.urlparse('https://example.com/bad\nurl')
    ParseResult(scheme='https', netloc='example.com', path='/bad\nurl', params='', query='', fragment='')

Behavior after::

    >>> urllib.parse.urlparse('https://example.com/bad\nurl')
    ParseResult(scheme='https', netloc='example.com', path='/badurl', params='', query='', fragment='')


.. _bpo43882: https://bugs.python.org/issue43882
.. _urllib.parse: https://docs.python.org/3/library/urllib.parse.html
.. _django PR#14349: https://github.com/django/django/pull/14349


Python 3.11
===========

See also: `what's new in Python 3.11`_

.. _what's new in Python 3.11:
   https://docs.python.org/3.11/whatsnew/3.11.html


Generator-based coroutine removal (asyncio.coroutine)
-----------------------------------------------------
Support for `generator-based coroutines`_ has been deprecated since
Python 3.8, and is finally removed in 3.11.  This usually results
in the following error::

    AttributeError: module 'asyncio' has no attribute 'coroutine'

The recommended solution is to use `PEP 492 coroutines`_.  They are
available since Python 3.5.  This means replacing
the ``@asyncio.coroutine`` decorator with ``async def`` keyword,
and ``yield from`` with ``await``.

For example, the following snippet::

    @asyncio.coroutine
    def foo():
        yield from asyncio.sleep(5)

would become::

    async def foo():
        await asyncio.sleep(5)


.. _generator-based coroutines:
   https://docs.python.org/3.10/library/asyncio-task.html#generator-based-coroutines
.. _PEP 492 coroutines:
   https://docs.python.org/3.10/library/asyncio-task.html#coroutines


inspect.getargspec() and inspect.formatargspec() removal
--------------------------------------------------------
The `inspect.getargspec()`_ (deprecated since Python 3.0)
and `inspect.formatargspec()`_ (deprecated since Python 3.5) functions
are both removed in Python 3.11.

The `inspect.getargspec()`_ function provides a legacy interface
to inspect the signature of callables.  It is replaced
by the object-oriented `inspect.signature()`_ API (available since
Python 3.3), or a mostly compatible `inspect.getfullargspec()`_ function
(available since Python 3.0).

For example, a trivial function would yield the following results::

    >>> def foo(p1, p2, /, kp3, kp4 = 10, kp5 = None, *args, **kwargs):
    ...     pass
    ...
    >>> inspect.getargspec(foo)
    ArgSpec(args=['p1', 'p2', 'kp3', 'kp4', 'kp5'],
            varargs='args',
            keywords='kwargs',
            defaults=(10, None))
    >>> inspect.getfullargspec(foo)
    FullArgSpec(args=['p1', 'p2', 'kp3', 'kp4', 'kp5'],
                varargs='args',
                varkw='kwargs',
                defaults=(10, None),
                kwonlyargs=[],
                kwonlydefaults=None,
                annotations={})
    >>> inspect.signature(foo)
    <Signature (p1, p2, /, kp3, kp4=10, kp5=None, *args, **kwargs)>

The named tuple returned by `inspect.getfullargspec()`_ starts with
the same information, except that the key used to hold the name
of ``**`` parameter is ``varkw`` rather than ``keywords``.
`inspect.signature()`_ returns a ``Signature`` object.

Both of the newer functions support keyword-only arguments and type
annotations::

    >>> def foo(p1: int, p2: str, /, kp3: str, kp4: int = 10,
    ...         kp5: float = None, *args, k6: str, k7: int = 12,
    ...         k8: float, **kwargs) -> float:
    ...     pass
    ...
    >>> inspect.getfullargspec(foo)
    FullArgSpec(args=['p1', 'p2', 'kp3', 'kp4', 'kp5'],
                varargs='args',
                varkw='kwargs',
                defaults=(10, None),
                kwonlyargs=['k6', 'k7', 'k8'],
                kwonlydefaults={'k7': 12},
                annotations={'return': <class 'float'>,
                             'p1': <class 'int'>,
                             'p2': <class 'str'>,
                             'kp3': <class 'str'>,
                             'kp4': <class 'int'>,
                             'kp5': <class 'float'>,
                             'k6': <class 'str'>,
                             'k7': <class 'int'>,
                             'k8': <class 'float'>})
    >>> inspect.signature(foo)
    <Signature (p1: int, p2: str, /, kp3: str, kp4: int = 10,
                kp5: float = None, *args, k6: str, k7: int = 12,
                k8: float, **kwargs) -> float>

One notable difference between `inspect.signature()`_ and the two other
functions is that the latter always include the 'self' argument
of method prototypes, while the former skips it if the method is bound
to an object.  That is::

    >>> class foo:
    ...     def x(self, bar):
    ...         pass
    ...
    >>> inspect.getargspec(foo.x)
    ArgSpec(args=['self', 'bar'], varargs=None, keywords=None, defaults=None)
    >>> inspect.getargspec(foo().x)
    ArgSpec(args=['self', 'bar'], varargs=None, keywords=None, defaults=None)
    >>> inspect.signature(foo.x)
    <Signature (self, bar)>
    >>> inspect.signature(foo().x)
    <Signature (bar)>

The `inspect.formatargspec()`_ function provides a pretty-formatted
argument spec from the tuple returned by `inspect.getfullargspec()`_
(or `inspect.getargspec()`_).  It is replaced by stringification
of ``Signature`` objects::

    >>> def foo(p1: int, p2: str, /, kp3: str, kp4: int = 10,
    ...         kp5: float = None, *args, k6: str, k7: int = 12,
    ...         k8: float, **kwargs) -> float:
    ...     pass
    ...
    >>> inspect.formatargspec(*inspect.getfullargspec(foo))
    '(p1: int, p2: str, kp3: str, kp4: int=10, kp5: float=None, '
    '*args, k6: str, k7: int=12, k8: float, **kwargs) -> float'
    >>> str(inspect.signature(foo))
    '(p1: int, p2: str, /, kp3: str, kp4: int = 10, kp5: float = None, '
    '*args, k6: str, k7: int = 12, k8: float, **kwargs) -> float'


.. _inspect.getargspec():
   https://docs.python.org/3.10/library/inspect.html#inspect.getargspec
.. _inspect.formatargspec():
   https://docs.python.org/3.10/library/inspect.html#inspect.formatargspec
.. _inspect.getfullargspec():
   https://docs.python.org/3.10/library/inspect.html#inspect.getfullargspec
.. _inspect.signature():
   https://docs.python.org/3.10/library/inspect.html#inspect.signature


Python 3.10
===========

See also: `what's new in Python 3.10`_

.. _what's new in Python 3.10:
   https://docs.python.org/3/whatsnew/3.10.html


configure: No package 'python-3.1' found
----------------------------------------
automake prior to 1.16.3 wrongly recognized Python 3.10 as 3.1.
As a result, build with Python 3.10 fails:

.. code-block:: console

    checking for python version... 3.1
    checking for python platform... linux
    checking for python script directory... ${prefix}/lib/python3.10/site-packages
    checking for python extension module directory... ${exec_prefix}/lib/python3.10/site-packages
    checking for PYTHON... no
    configure: error: Package requirements (python-3.1) were not met:

    No package 'python-3.1' found

    Consider adjusting the PKG_CONFIG_PATH environment variable if you
    installed software in a non-standard prefix.

    Alternatively, you may set the environment variables PYTHON_CFLAGS
    and PYTHON_LIBS to avoid the need to call pkg-config.
    See the pkg-config man page for more details.
    Error: Process completed with exit code 1.

To resolve this in ebuild, you need to autoreconf with the Gentoo
distribution of automake::

    inherit autotools

    # ...

    src_prepare() {
        default
        eautoreconf
    }

The upstream fix is to create new distfiles using automake-1.16.3+.


distutils.sysconfig deprecation
-------------------------------
Upstream intends to remove distutils by Python 3.12.  Python 3.10 starts
throwing deprecation warnings for various distutils modules.
The distutils.sysconfig is usually easy to port.

The following table summarizes replacements for common path getters.

  =================================== ==================================
  distutils.sysconfig call            sysconfig replacement
  =================================== ==================================
  ``get_python_inc(False)``           ``get_path("include")``
  ``get_python_inc(True)``            ``get_path("platinclude")``
  ``get_python_lib(False, False)``    ``get_path("purelib")``
  ``get_python_lib(True, False)``     ``get_path("platlib")``
  ``get_python_lib(False, True)``     ``get_path("stdlib")``
  ``get_python_lib(True, True)``      ``get_path("platstdlib")``
  =================================== ==================================

For both functions, omitted parameters default to ``False``.  There is
no trivial replacement for the variants with ``prefix`` argument.


Python 3.9
==========

See also: `what's new in Python 3.9`_

.. _what's new in Python 3.9:
   https://docs.python.org/3/whatsnew/3.9.html


base64.encodestring / base64.decodestring removal
-------------------------------------------------
Python 3.9 removes the deprecated ``base64.encodestring()``
and ``base64.decodestring()`` functions.  While they were deprecated
since Python 3.1, many packages still use them today.

The drop-in Python 3.1+ replacements are ``base64.encodebytes()``
and ``base64.decodebytes()``.  Note that contrary to the names, the old
functions were simply aliases to the byte variants in Python 3
and *required* the arguments to be ``bytes`` anyway.

If compatibility with Python 2 is still desired, then the byte variants
ought to be called on 3.1+ and string variants before that.  The old
variants accept both byte and unicode strings on Python 2.

Example compatibility import::

    import sys

    if sys.version_info >= (3, 1):
        from base64 import encodebytes as b64_encodebytes
    else:
        from base64 import encodestring as b64_encodebytes

Note that the ``base64`` module also provides ``b64encode()``
and ``b64decode()`` functions that were not renamed.  ``b64decode()``
can be used as a drop-in replacement for ``decodebytes()``.  However,
``b64encode()`` does not insert newlines to split the output
like ``encodebytes()`` does, and instead returns a single line
of base64-encoded data for any length of output.


Python 3.8
==========

See also: `what's new in Python 3.8`_

.. _what's new in Python 3.8:
   https://docs.python.org/3/whatsnew/3.8.html


python-config and pkg-config no longer list Python library by default
---------------------------------------------------------------------
Until Python 3.7, the ``python-X.Y`` pkg-config file and python-config
tool listed the Python library.  Starting with 3.8, this is no longer
the case.  If you are building Python extensions, this is fine (they
are not supposed to link directly to libpython).

If you are building programs that need to embed the Python interpreter,
new ``python-X.Y-embed`` pkg-config file and ``--embed`` parameter
are provided for the purpose.

.. code-block:: console

    $ pkg-config --libs python-3.7
    -lpython3.7m
    $ pkg-config --libs python-3.8

    $ pkg-config --libs python-3.8-embed
    -lpython3.8

To achieve backwards compatibility, you should query
``python-X.Y-embed`` first and fall back to ``python-X.Y``.


Replacing the toml package
==========================

The old toml_ package is no longer maintained.  It was last released
in November 2020 and it was never updated to implement TOML 1.0.
The recommended alternatives are:

- the built-in tomllib_ module (since Python 3.11) with fallback to
  tomli_ package for reading TOML files

- the tomli-w_ package for writing TOML files

- the tomlkit_ package for editing already existing TOML files
  while preserving style


Porting to tomllib/tomli without toml fallback
----------------------------------------------
Using a combination of tomllib_ and tomli_ is the recommended approach
for packages that only read TOML files, or both read and write them
but do not need to preserve style.  The tomllib module is available
since Python 3.11, while tomli versions providing a compatible API
are compatible with Python 3.6 and newer.

The key differences between toml_ and tomllib/tomli are:

- the ``load()`` function accepts only a file object open for reading
  in binary mode whereas toml expects a path or a file object open
  for reading in text mode

- the exception raised for invalid input is named ``TOMLDecodeError``
  where it is named ``TomlDecodeError`` in toml

For example, the following code::

    import toml

    try:
        d1 = toml.load("in1.toml")
    except toml.TomlDecodeError:
        d1 = None

    with open("in2.toml", "r") as f:
        d2 = toml.load(f)

    d3 = toml.loads('test = "foo"\n')

would normally be written as::

    import sys

    if sys.version_info >= (3, 11):
        import tomllib
    else:
        import tomli as tomllib

    try:
        # tomllib does not accept paths
        with open("in1.toml", "rb") as f:
            d1 = tomllib.load(f)
    # the exception uses uppercase "TOML"
    except tomllib.TOMLDecodeError:
        d1 = None

    # the file must be open in binary mode
    with open("in2.toml", "rb") as f:
        d2 = tomllib.load(f)

    d3 = tomllib.loads('test = "foo"\n')


Porting to tomllib/tomli with toml fallback
-------------------------------------------
If upstream insists on preserving compatibility with EOL versions
of Python, it is possible to use a combination of tomllib_, tomli_
and toml_.  Unfortunately, the incompatibilites in API need to be taken
into consideration.

For example, a backwards compatible code for loading a TOML file could
look like the following::

    import sys

    try:
        if sys.version_info >= (3, 11):
            import tomllib
        else:
            import tomli as tomllib

        try:
            with open("in1.toml", "rb") as f:
                d1 = tomllib.load(f)
        except tomllib.TOMLDecodeError:
            d1 = None
    except ImportError:
        import toml

        try:
            with open("in1.toml", "r") as f:
                d1 = toml.load(f)
        except toml.TomlDecodeError:
            d1 = None


Porting to tomli-w
------------------
tomli-w_ provides a minimal module for dumping TOML files.

The key differences between toml_ and tomli-w are:

- the ``dump()`` function takes a file object open for writing in binary
  mode whereas toml expected a file object open for writing in text mode

- providing a custom encoder instance is not supported

For example, the following code::

    import toml

    with open("out.toml", "w") as f:
        toml.dump({"test": "data"}, f)

would be replaced by::

    import tomli_w

    with open("out.toml", "wb") as f:
        tomli_w.dump({"test": "data"}, f)

Note that when both reading and writing TOML files is necessary, two
modules need to be imported and used separately rather than one.


.. _toml: https://pypi.org/project/toml/
.. _tomllib: https://docs.python.org/3.11/library/tomllib.html
.. _tomli: https://pypi.org/project/tomli/
.. _tomli-w: https://pypi.org/project/tomli-w/
.. _tomlkit: https://pypi.org/project/tomlkit/