Usage

Specifying Files And Directories To Convert

To convert a single file:

$ cat myscript.py
print(hello := 'world')
$ poseur myscript.py
Now converting: '/path/to/project/myscript.py'
$ cat myscript.py  # file overwritten with conversion result
if False:
    hello = NotImplemented


def __poseur_wrapper_hello_5adbf5ee911449cba75e35b9ef97ea80(expr):
    """Wrapper function for assignment expression."""
    global hello
    hello = expr
    return hello


print(__poseur_wrapper_hello_5adbf5ee911449cba75e35b9ef97ea80('world'))

To convert the whole project at the current working directory (overwrites all Python source files inside):

$ poseur .

Multiple files and directories may be supplied at the same time:

$ poseur script_without_py_extension /path/to/another/project

Note

When converting a directory, poseur will recursively find all the Python source files in the directory (and its subdirectories, if any). Whether a file is a Python source file is determined by its file extension (.py or .pyw). If you want to convert a file without a Python extension, you will need to explicitly specify it in the argument list.

If you prefer a side-effect free behavior (do not overwrite files), you can use the simple mode.

Simple mode with no arguments (read from stdin, write to stdout):

$ printf 'print(hello := "world")\n' | python3 poseur.py -s
if False:
    hello = NotImplemented


def __poseur_wrapper_hello_fbf3a9dabd2b40348815e3f2b22a1683(expr):
    """Wrapper function for assignment expression."""
    global hello
    hello = expr
    return hello

print(__poseur_wrapper_hello_fbf3a9dabd2b40348815e3f2b22a1683("world"))

Simple mode with a file name argument (read from file, write to stdout):

$ cat myscript.py
print(hello := 'world')
$ poseur -s myscript.py
if False:
    hello = NotImplemented


def __poseur_wrapper_hello_d1e6c2a11a76400aa9745bd90b3fb52a(expr):
    """Wrapper function for assignment expression."""
    global hello
    hello = expr
    return hello

print(__poseur_wrapper_hello_d1e6c2a11a76400aa9745bd90b3fb52a('world'))
$ cat myscript.py
print(hello := 'world')

In simple mode, no file names shall be provided via positional arguments.

Archiving And Recovering Files

If you are not using the simple mode, poseur overwrites Python source files, which could potentially cause data loss. Therefore, a built-in archiving functionality is enabled by default. The original copies of the Python source files to be converted will be packed into an archive file and placed under the archive subdirectory of the current working directory.

To opt out of archiving, use the CLI option -na (--no-archive), or set environment variable POSEUR_DO_ARCHIVE=0.

To use an alternative name for the archive directory (other than archive), use the CLI option -k (--archive-path), or set the environment variable POSEUR_ARCHIVE_PATH.

To recover files from an archive file, use the CLI option -r (--recover):

$ poseur -r archive/archive-20200814222751-f3a514d40d69c6d5.tar.gz
Recovered files from archive: 'archive/archive-20200814222751-f3a514d40d69c6d5.tar.gz'
$ ls archive/
archive-20200814222751-f3a514d40d69c6d5.tar.gz

By default, the archive file is still retained after recovering from it. If you would like it to be removed after recovery, specify the CLI option -r2:

$ poseur -r archive/archive-20200814222751-f3a514d40d69c6d5.tar.gz -r2
Recovered files from archive: 'archive/archive-20200814222751-f3a514d40d69c6d5.tar.gz'
$ ls archive/

If you also want to remove the archive directory if it becomes empty, specify the CLI option -r3:

$ poseur -r archive/archive-20200814222751-f3a514d40d69c6d5.tar.gz -r3
Recovered files from archive: 'archive/archive-20200814222751-f3a514d40d69c6d5.tar.gz'
$ ls archive/
ls: cannot access 'archive/': No such file or directory

Warning

To improve stability of file recovery, the archive file records the original absolute paths of the Python source files. Thus, file recovery is only guaranteed to work correctly on the same machine where the archive file was created. Never perform the recovery operation on an arbitrary untrusted archive file. Doing so may allow attackers to overwrite any files in the system.

Conversion Options

By default, poseur automatically detects file line endings and use the same line ending for code insertion. If you want to manually specify the line ending to be used, use the CLI option -l (--linesep) or the POSEUR_LINESEP environment variable.

By default, poseur automatically detects file indentations and use the same indentation for code insertion. If you want to manually specify the indentation to be used, use the CLI option -t (--indentation) or the POSEUR_INDENTATION environment variable.

By default, poseur parse Python source files as the latest version. If you want to manually specify a version for parsing, use the CLI option -vs (-vf, --source-version, --from-version) or the POSEUR_SOURCE_VERSION environment variable.

By default, code insertion of poseur conforms to PEP 8. To opt out and get a more compact result, specify the CLI option -n8 (--no-pep8) or set environment variable POSEUR_PEP8=0.

Runtime Options

Specify the CLI option -q (--quiet) or set environment variable POSEUR_QUIET=1 to run in quiet mode.

Specify the CLI option -C (--concurrency) or set environment variable POSEUR_CONCURRENCY to specify the number of concurrent worker processes for conversion.

Use the --dry-run CLI option to list the files to be converted without actually performing conversion and archiving.

By running poseur --help, you can see the current values of all the options, based on their default values and your environment variables.

API Usage

If you want to programmatically invoke poseur, you may want to look at API Reference. The poseur.convert() and poseur.poseur() functions should be most commonly used.

Disutils/Setuptools Integration

poseur can also be directly integrated within your setup.py script to dynamically convert assignment expressions upon installation:

import subprocess
import sys

try:
    from setuptools import setup
    from setuptools.command.build_py import build_py
except ImportError:
    from distutils.core import setup
    from distutils.command.build_py import build_py

version_info = sys.version_info[:2]


class build(build_py):
    """Add on-build backport code conversion."""

    def run(self):
        if version_info < (3, 8):
            try:
                subprocess.check_call(  # nosec
                    [sys.executable, '-m', 'poseur', '--no-archive', 'PACKAGENAME']
                )
            except subprocess.CalledProcessError as error:
                print('Failed to perform assignment expression backport compiling.'
                      'Please consider manually install `bpc-poseur` and try again.', file=sys.stderr)
                sys.exit(error.returncode)
        build_py.run(self)


setup(
    ...
    setup_requires=[
        'bpc-poseur; python_version < "3.8"',
    ],
    cmdclass={
        'build_py': build,
    },
)

Or, as PEP 518 proposed, you may simply add bpc-poseur to the requires list from the [build-system] section in the pyproject.toml file:

[built-system]
# Minimum requirements for the build system to execute.
requires = ["setuptools", "wheel", "bpc-poseur"]  # PEP 508 specifications.
...