Blame SOURCES/pyproject_buildrequires.py

20b2b6
import glob
20b2b6
import io
7ef706
import os
7ef706
import sys
7ef706
import importlib.metadata
7ef706
import argparse
5d3ed2
import tempfile
7ef706
import traceback
7ef706
import contextlib
7ef706
import json
7ef706
import subprocess
7ef706
import re
7ef706
import tempfile
7ef706
import email.parser
7ef706
import pathlib
20b2b6
import zipfile
7ef706
7ef706
from pyproject_requirements_txt import convert_requirements_txt
7ef706
7ef706
7ef706
# Some valid Python version specifiers are not supported.
7ef706
# Allow only the forms we know we can handle.
7ef706
VERSION_RE = re.compile(r'[a-zA-Z0-9.-]+(\.\*)?')
7ef706
7ef706
7ef706
class EndPass(Exception):
7ef706
    """End current pass of generating requirements"""
7ef706
7ef706
7ef706
# nb: we don't use functools.partial to be able to use pytest's capsys
7ef706
# see https://github.com/pytest-dev/pytest/issues/8900
7ef706
def print_err(*args, **kwargs):
7ef706
    kwargs.setdefault('file', sys.stderr)
7ef706
    print(*args, **kwargs)
7ef706
7ef706
7ef706
try:
7ef706
    from packaging.requirements import Requirement, InvalidRequirement
7ef706
    from packaging.utils import canonicalize_name
7ef706
except ImportError as e:
7ef706
    print_err('Import error:', e)
7ef706
    # already echoed by the %pyproject_buildrequires macro
7ef706
    sys.exit(0)
7ef706
7ef706
# uses packaging, needs to be imported after packaging is verified to be present
7ef706
from pyproject_convert import convert
7ef706
7ef706
7ef706
@contextlib.contextmanager
7ef706
def hook_call():
5d3ed2
    """Context manager that records all stdout content (on FD level)
5d3ed2
    and prints it to stderr at the end, with a 'HOOK STDOUT: ' prefix."""
5d3ed2
    tmpfile = io.TextIOWrapper(
5d3ed2
        tempfile.TemporaryFile(buffering=0),
5d3ed2
        encoding='utf-8',
5d3ed2
        errors='replace',
5d3ed2
        write_through=True,
5d3ed2
    )
5d3ed2
5d3ed2
    stdout_fd = 1
5d3ed2
    stdout_fd_dup = os.dup(stdout_fd)
5d3ed2
    stdout_orig = sys.stdout
5d3ed2
5d3ed2
    # begin capture
5d3ed2
    sys.stdout = tmpfile
5d3ed2
    os.dup2(tmpfile.fileno(), stdout_fd)
5d3ed2
5d3ed2
    try:
7ef706
        yield
5d3ed2
    finally:
5d3ed2
        # end capture
5d3ed2
        sys.stdout = stdout_orig
5d3ed2
        os.dup2(stdout_fd_dup, stdout_fd)
5d3ed2
5d3ed2
        tmpfile.seek(0)  # rewind
5d3ed2
        for line in tmpfile:
5d3ed2
            print_err('HOOK STDOUT:', line, end='')
5d3ed2
5d3ed2
        tmpfile.close()
7ef706
7ef706
7ef706
def guess_reason_for_invalid_requirement(requirement_str):
7ef706
    if ':' in requirement_str:
7ef706
        message = (
7ef706
            'It might be an URL. '
7ef706
            '%pyproject_buildrequires cannot handle all URL-based requirements. '
7ef706
            'Add PackageName@ (see PEP 508) to the URL to at least require any version of PackageName.'
7ef706
        )
7ef706
        if '@' in requirement_str:
7ef706
            message += ' (but note that URLs might not work well with other features)'
7ef706
        return message
7ef706
    if '/' in requirement_str:
7ef706
        return (
7ef706
            'It might be a local path. '
7ef706
            '%pyproject_buildrequires cannot handle local paths as requirements. '
7ef706
            'Use an URL with PackageName@ (see PEP 508) to at least require any version of PackageName.'
7ef706
        )
7ef706
    # No more ideas
7ef706
    return None
7ef706
7ef706
7ef706
class Requirements:
7ef706
    """Requirement printer"""
7ef706
    def __init__(self, get_installed_version, extras=None,
7ef706
                 generate_extras=False, python3_pkgversion='3'):
7ef706
        self.get_installed_version = get_installed_version
7ef706
        self.extras = set()
7ef706
7ef706
        if extras:
7ef706
            for extra in extras:
7ef706
                self.add_extras(*extra.split(','))
7ef706
7ef706
        self.missing_requirements = False
7ef706
7ef706
        self.generate_extras = generate_extras
7ef706
        self.python3_pkgversion = python3_pkgversion
7ef706
7ef706
    def add_extras(self, *extras):
7ef706
        self.extras |= set(e.strip() for e in extras)
7ef706
7ef706
    @property
7ef706
    def marker_envs(self):
7ef706
        if self.extras:
7ef706
            return [{'extra': e} for e in sorted(self.extras)]
7ef706
        return [{'extra': ''}]
7ef706
5d3ed2
    def evaluate_all_environments(self, requirement):
7ef706
        for marker_env in self.marker_envs:
7ef706
            if requirement.marker.evaluate(environment=marker_env):
7ef706
                return True
7ef706
        return False
7ef706
7ef706
    def add(self, requirement_str, *, source=None):
7ef706
        """Output a Python-style requirement string as RPM dep"""
7ef706
        print_err(f'Handling {requirement_str} from {source}')
7ef706
7ef706
        try:
7ef706
            requirement = Requirement(requirement_str)
7ef706
        except InvalidRequirement:
7ef706
            hint = guess_reason_for_invalid_requirement(requirement_str)
7ef706
            message = f'Requirement {requirement_str!r} from {source} is invalid.'
7ef706
            if hint:
7ef706
                message += f' Hint: {hint}'
7ef706
            raise ValueError(message)
7ef706
7ef706
        if requirement.url:
7ef706
            print_err(
7ef706
                f'WARNING: Simplifying {requirement_str!r} to {requirement.name!r}.'
7ef706
            )
7ef706
7ef706
        name = canonicalize_name(requirement.name)
7ef706
        if (requirement.marker is not None and
5d3ed2
                not self.evaluate_all_environments(requirement)):
7ef706
            print_err(f'Ignoring alien requirement:', requirement_str)
7ef706
            return
7ef706
7ef706
        # We need to always accept pre-releases as satisfying the requirement
7ef706
        # Otherwise e.g. installed cffi version 1.15.0rc2 won't even satisfy the requirement for "cffi"
7ef706
        # https://bugzilla.redhat.com/show_bug.cgi?id=2014639#c3
7ef706
        requirement.specifier.prereleases = True
7ef706
7ef706
        try:
7ef706
            # TODO: check if requirements with extras are satisfied
7ef706
            installed = self.get_installed_version(requirement.name)
7ef706
        except importlib.metadata.PackageNotFoundError:
7ef706
            print_err(f'Requirement not satisfied: {requirement_str}')
7ef706
            installed = None
7ef706
        if installed and installed in requirement.specifier:
7ef706
            print_err(f'Requirement satisfied: {requirement_str}')
7ef706
            print_err(f'   (installed: {requirement.name} {installed})')
7ef706
            if requirement.extras:
7ef706
                print_err(f'   (extras are currently not checked)')
7ef706
        else:
7ef706
            self.missing_requirements = True
7ef706
7ef706
        if self.generate_extras:
7ef706
            extra_names = [f'{name}[{extra.lower()}]' for extra in sorted(requirement.extras)]
7ef706
        else:
7ef706
            extra_names = []
7ef706
7ef706
        for name in [name] + extra_names:
7ef706
            together = []
7ef706
            for specifier in sorted(
7ef706
                requirement.specifier,
7ef706
                key=lambda s: (s.operator, s.version),
7ef706
            ):
7ef706
                if not VERSION_RE.fullmatch(str(specifier.version)):
7ef706
                    raise ValueError(
7ef706
                        f'Unknown character in version: {specifier.version}. '
7ef706
                        + '(This might be a bug in pyproject-rpm-macros.)',
7ef706
                    )
7ef706
                together.append(convert(python3dist(name, python3_pkgversion=self.python3_pkgversion),
7ef706
                                        specifier.operator, specifier.version))
7ef706
            if len(together) == 0:
7ef706
                print(python3dist(name,
7ef706
                                  python3_pkgversion=self.python3_pkgversion))
7ef706
            elif len(together) == 1:
7ef706
                print(together[0])
7ef706
            else:
7ef706
                print(f"({' with '.join(together)})")
7ef706
7ef706
    def check(self, *, source=None):
7ef706
        """End current pass if any unsatisfied dependencies were output"""
7ef706
        if self.missing_requirements:
7ef706
            print_err(f'Exiting dependency generation pass: {source}')
7ef706
            raise EndPass(source)
7ef706
7ef706
    def extend(self, requirement_strs, **kwargs):
7ef706
        """add() several requirements"""
7ef706
        for req_str in requirement_strs:
7ef706
            self.add(req_str, **kwargs)
7ef706
7ef706
20b2b6
def toml_load(opened_binary_file):
7ef706
    try:
20b2b6
        # tomllib is in the standard library since 3.11.0b1
20b2b6
        import tomllib as toml_module
20b2b6
        load_from = opened_binary_file
20b2b6
    except ImportError:
7ef706
        try:
20b2b6
            # note: we could use tomli here,
20b2b6
            # but for backwards compatibility with RHEL 9, we use toml instead
20b2b6
            import toml as toml_module
20b2b6
            load_from = io.TextIOWrapper(opened_binary_file, encoding='utf-8')
7ef706
        except ImportError as e:
7ef706
            print_err('Import error:', e)
7ef706
            # already echoed by the %pyproject_buildrequires macro
7ef706
            sys.exit(0)
20b2b6
    return toml_module.load(load_from)
20b2b6
20b2b6
20b2b6
def get_backend(requirements):
20b2b6
    try:
20b2b6
        f = open('pyproject.toml', 'rb')
20b2b6
    except FileNotFoundError:
20b2b6
        pyproject_data = {}
20b2b6
    else:
7ef706
        with f:
20b2b6
            pyproject_data = toml_load(f)
7ef706
7ef706
    buildsystem_data = pyproject_data.get('build-system', {})
7ef706
    requirements.extend(
7ef706
        buildsystem_data.get('requires', ()),
7ef706
        source='build-system.requires',
7ef706
    )
7ef706
7ef706
    backend_name = buildsystem_data.get('build-backend')
7ef706
    if not backend_name:
7ef706
        # https://www.python.org/dev/peps/pep-0517/:
7ef706
        # If the pyproject.toml file is absent, or the build-backend key is
7ef706
        # missing, the source tree is not using this specification, and tools
7ef706
        # should revert to the legacy behaviour of running setup.py
7ef706
        # (either directly, or by implicitly invoking the [following] backend).
7ef706
        # If setup.py is also not present program will mimick pip's behavior
7ef706
        # and end with an error.
7ef706
        if not os.path.exists('setup.py'):
7ef706
            raise FileNotFoundError('File "setup.py" not found for legacy project.')
7ef706
        backend_name = 'setuptools.build_meta:__legacy__'
7ef706
7ef706
        # Note: For projects without pyproject.toml, this was already echoed
7ef706
        # by the %pyproject_buildrequires macro, but this also handles cases
7ef706
        # with pyproject.toml without a specified build backend.
7ef706
        # If the default requirements change, also change them in the macro!
7ef706
        requirements.add('setuptools >= 40.8', source='default build backend')
7ef706
        requirements.add('wheel', source='default build backend')
7ef706
7ef706
    requirements.check(source='build backend')
7ef706
7ef706
    backend_path = buildsystem_data.get('backend-path')
7ef706
    if backend_path:
7ef706
        # PEP 517 example shows the path as a list, but some projects don't follow that
7ef706
        if isinstance(backend_path, str):
7ef706
            backend_path = [backend_path]
7ef706
        sys.path = backend_path + sys.path
7ef706
7ef706
    module_name, _, object_name = backend_name.partition(":")
7ef706
    backend_module = importlib.import_module(module_name)
7ef706
7ef706
    if object_name:
7ef706
        return getattr(backend_module, object_name)
7ef706
7ef706
    return backend_module
7ef706
7ef706
7ef706
def generate_build_requirements(backend, requirements):
7ef706
    get_requires = getattr(backend, 'get_requires_for_build_wheel', None)
7ef706
    if get_requires:
7ef706
        with hook_call():
7ef706
            new_reqs = get_requires()
7ef706
        requirements.extend(new_reqs, source='get_requires_for_build_wheel')
7ef706
        requirements.check(source='get_requires_for_build_wheel')
7ef706
7ef706
20b2b6
def requires_from_metadata_file(metadata_file):
20b2b6
    message = email.parser.Parser().parse(metadata_file, headersonly=True)
20b2b6
    return {k: message.get_all(k, ()) for k in ('Requires', 'Requires-Dist')}
20b2b6
20b2b6
20b2b6
def generate_run_requirements_hook(backend, requirements):
7ef706
    hook_name = 'prepare_metadata_for_build_wheel'
7ef706
    prepare_metadata = getattr(backend, hook_name, None)
7ef706
    if not prepare_metadata:
7ef706
        raise ValueError(
20b2b6
            'The build backend cannot provide build metadata '
20b2b6
            '(incl. runtime requirements) before build. '
20b2b6
            'Use the provisional -w flag to build the wheel and parse the metadata from it, '
20b2b6
            'or use the -R flag not to generate runtime dependencies.'
7ef706
        )
7ef706
    with hook_call():
7ef706
        dir_basename = prepare_metadata('.')
20b2b6
    with open(dir_basename + '/METADATA') as metadata_file:
20b2b6
        for key, requires in requires_from_metadata_file(metadata_file).items():
20b2b6
            requirements.extend(requires, source=f'hook generated metadata: {key}')
20b2b6
20b2b6
20b2b6
def find_built_wheel(wheeldir):
20b2b6
    wheels = glob.glob(os.path.join(wheeldir, '*.whl'))
20b2b6
    if not wheels:
20b2b6
        return None
20b2b6
    if len(wheels) > 1:
20b2b6
        raise RuntimeError('Found multiple wheels in %{_pyproject_wheeldir}, '
20b2b6
                           'this is not supported with %pyproject_buildrequires -w.')
20b2b6
    return wheels[0]
20b2b6
20b2b6
20b2b6
def generate_run_requirements_wheel(backend, requirements, wheeldir):
20b2b6
    # Reuse the wheel from the previous round of %pyproject_buildrequires (if it exists)
20b2b6
    wheel = find_built_wheel(wheeldir)
20b2b6
    if not wheel:
20b2b6
        import pyproject_wheel
20b2b6
        returncode = pyproject_wheel.build_wheel(wheeldir=wheeldir, stdout=sys.stderr)
20b2b6
        if returncode != 0:
20b2b6
            raise RuntimeError('Failed to build the wheel for %pyproject_buildrequires -w.')
20b2b6
        wheel = find_built_wheel(wheeldir)
20b2b6
    if not wheel:
20b2b6
        raise RuntimeError('Cannot locate the built wheel for %pyproject_buildrequires -w.')
20b2b6
20b2b6
    print_err(f'Reading metadata from {wheel}')
20b2b6
    with zipfile.ZipFile(wheel) as wheelfile:
20b2b6
        for name in wheelfile.namelist():
20b2b6
            if name.count('/') == 1 and name.endswith('.dist-info/METADATA'):
20b2b6
                with io.TextIOWrapper(wheelfile.open(name), encoding='utf-8') as metadata_file:
20b2b6
                    for key, requires in requires_from_metadata_file(metadata_file).items():
20b2b6
                        requirements.extend(requires, source=f'built wheel metadata: {key}')
20b2b6
                break
20b2b6
        else:
20b2b6
            raise RuntimeError('Could not find *.dist-info/METADATA in built wheel.')
20b2b6
20b2b6
20b2b6
def generate_run_requirements(backend, requirements, *, build_wheel, wheeldir):
20b2b6
    if build_wheel:
20b2b6
        generate_run_requirements_wheel(backend, requirements, wheeldir)
20b2b6
    else:
20b2b6
        generate_run_requirements_hook(backend, requirements)
7ef706
7ef706
7ef706
def generate_tox_requirements(toxenv, requirements):
7ef706
    toxenv = ','.join(toxenv)
7ef706
    requirements.add('tox-current-env >= 0.0.6', source='tox itself')
7ef706
    requirements.check(source='tox itself')
7ef706
    with tempfile.NamedTemporaryFile('r') as deps, \
7ef706
        tempfile.NamedTemporaryFile('r') as extras, \
7ef706
            tempfile.NamedTemporaryFile('r') as provision:
7ef706
        r = subprocess.run(
7ef706
            [sys.executable, '-m', 'tox',
7ef706
             '--print-deps-to', deps.name,
7ef706
             '--print-extras-to', extras.name,
7ef706
             '--no-provision', provision.name,
20b2b6
             '-q', '-r', '-e', toxenv],
7ef706
            check=False,
7ef706
            encoding='utf-8',
7ef706
            stdout=subprocess.PIPE,
7ef706
            stderr=subprocess.STDOUT,
7ef706
        )
7ef706
        if r.stdout:
7ef706
            print_err(r.stdout, end='')
7ef706
7ef706
        provision_content = provision.read()
7ef706
        if provision_content and r.returncode != 0:
7ef706
            provision_requires = json.loads(provision_content)
7ef706
            if 'minversion' in provision_requires:
7ef706
                requirements.add(f'tox >= {provision_requires["minversion"]}',
7ef706
                                 source='tox provision (minversion)')
7ef706
            if 'requires' in provision_requires:
7ef706
                requirements.extend(provision_requires["requires"],
7ef706
                                    source='tox provision (requires)')
7ef706
            requirements.check(source='tox provision')  # this terminates the script
7ef706
            raise RuntimeError(
7ef706
                'Dependencies requested by tox provisioning appear installed, '
7ef706
                'but tox disagreed.')
7ef706
        else:
7ef706
            r.check_returncode()
7ef706
7ef706
        deplines = deps.read().splitlines()
7ef706
        packages = convert_requirements_txt(deplines)
7ef706
        requirements.add_extras(*extras.read().splitlines())
7ef706
        requirements.extend(packages,
7ef706
                            source=f'tox --print-deps-only: {toxenv}')
7ef706
7ef706
7ef706
def python3dist(name, op=None, version=None, python3_pkgversion="3"):
7ef706
    prefix = f"python{python3_pkgversion}dist"
7ef706
7ef706
    if op is None:
7ef706
        if version is not None:
7ef706
            raise AssertionError('op and version go together')
7ef706
        return f'{prefix}({name})'
7ef706
    else:
7ef706
        return f'{prefix}({name}) {op} {version}'
7ef706
7ef706
7ef706
def generate_requires(
20b2b6
    *, include_runtime=False, build_wheel=False, wheeldir=None, toxenv=None, extras=None,
7ef706
    get_installed_version=importlib.metadata.version,  # for dep injection
7ef706
    generate_extras=False, python3_pkgversion="3", requirement_files=None, use_build_system=True
7ef706
):
7ef706
    """Generate the BuildRequires for the project in the current directory
7ef706
7ef706
    This is the main Python entry point.
7ef706
    """
7ef706
    requirements = Requirements(
7ef706
        get_installed_version, extras=extras or [],
7ef706
        generate_extras=generate_extras,
7ef706
        python3_pkgversion=python3_pkgversion
7ef706
    )
7ef706
7ef706
    try:
7ef706
        if (include_runtime or toxenv) and not use_build_system:
7ef706
            raise ValueError('-N option cannot be used in combination with -r, -e, -t, -x options')
7ef706
        if requirement_files:
7ef706
            for req_file in requirement_files:
7ef706
                requirements.extend(
7ef706
                    convert_requirements_txt(req_file, pathlib.Path(req_file.name)),
7ef706
                    source=f'requirements file {req_file.name}'
7ef706
                )
7ef706
            requirements.check(source='all requirements files')
7ef706
        if use_build_system:
7ef706
            backend = get_backend(requirements)
7ef706
            generate_build_requirements(backend, requirements)
7ef706
        if toxenv:
7ef706
            include_runtime = True
7ef706
            generate_tox_requirements(toxenv, requirements)
7ef706
        if include_runtime:
20b2b6
            generate_run_requirements(backend, requirements, build_wheel=build_wheel, wheeldir=wheeldir)
7ef706
    except EndPass:
7ef706
        return
7ef706
7ef706
7ef706
def main(argv):
7ef706
    parser = argparse.ArgumentParser(
5d3ed2
        description='Generate BuildRequires for a Python project.',
5d3ed2
        prog='%pyproject_buildrequires',
5d3ed2
        add_help=False,
5d3ed2
    )
5d3ed2
    parser.add_argument(
5d3ed2
        '--help', action='help',
5d3ed2
        default=argparse.SUPPRESS,
5d3ed2
        help=argparse.SUPPRESS,
7ef706
    )
7ef706
    parser.add_argument(
7ef706
        '-r', '--runtime', action='store_true', default=True,
5d3ed2
        help=argparse.SUPPRESS,  # Generate run-time requirements (backwards-compatibility only)
7ef706
    )
7ef706
    parser.add_argument(
5d3ed2
        '--generate-extras', action='store_true',
5d3ed2
        help=argparse.SUPPRESS,
20b2b6
    )
20b2b6
    parser.add_argument(
5d3ed2
        '-p', '--python3_pkgversion', metavar='PYTHON3_PKGVERSION',
5d3ed2
        default="3", help=argparse.SUPPRESS,
20b2b6
    )
20b2b6
    parser.add_argument(
5d3ed2
        '--wheeldir', metavar='PATH', default=None,
5d3ed2
        help=argparse.SUPPRESS,
7ef706
    )
7ef706
    parser.add_argument(
5d3ed2
        '-x', '--extras', metavar='EXTRAS', action='append',
5d3ed2
        help='comma separated list of "extras" for runtime requirements '
5d3ed2
             '(e.g. -x testing,feature-x) (implies --runtime, can be repeated)',
7ef706
    )
7ef706
    parser.add_argument(
7ef706
        '-t', '--tox', action='store_true',
7ef706
        help=('generate test tequirements from tox environment '
7ef706
              '(implies --runtime)'),
7ef706
    )
7ef706
    parser.add_argument(
5d3ed2
        '-e', '--toxenv', metavar='TOXENVS', action='append',
5d3ed2
        help=('specify tox environments (comma separated and/or repeated)'
5d3ed2
              '(implies --tox)'),
7ef706
    )
7ef706
    parser.add_argument(
5d3ed2
        '-w', '--wheel', action='store_true', default=False,
5d3ed2
        help=('Generate run-time requirements by building the wheel '
5d3ed2
              '(useful for build backends without the prepare_metadata_for_build_wheel hook)'),
7ef706
    )
7ef706
    parser.add_argument(
5d3ed2
        '-R', '--no-runtime', action='store_false', dest='runtime',
5d3ed2
        help="Don't generate run-time requirements (implied by -N)",
7ef706
    )
7ef706
    parser.add_argument(
7ef706
        '-N', '--no-use-build-system', dest='use_build_system',
7ef706
        action='store_false', help='Use -N to indicate that project does not use any build system',
7ef706
    )
7ef706
    parser.add_argument(
5d3ed2
        'requirement_files', nargs='*', type=argparse.FileType('r'),
5d3ed2
        metavar='REQUIREMENTS.TXT',
7ef706
        help=('Add buildrequires from file'),
7ef706
    )
7ef706
7ef706
    args = parser.parse_args(argv)
7ef706
7ef706
    if not args.use_build_system:
7ef706
        args.runtime = False
7ef706
20b2b6
    if args.wheel:
20b2b6
        if not args.wheeldir:
20b2b6
            raise ValueError('--wheeldir must be set when -w.')
20b2b6
7ef706
    if args.toxenv:
7ef706
        args.tox = True
7ef706
7ef706
    if args.tox:
7ef706
        args.runtime = True
7ef706
        if not args.toxenv:
7ef706
            _default = f'py{sys.version_info.major}{sys.version_info.minor}'
7ef706
            args.toxenv = [os.getenv('RPM_TOXENV', _default)]
7ef706
7ef706
    if args.extras:
7ef706
        args.runtime = True
7ef706
7ef706
    try:
7ef706
        generate_requires(
7ef706
            include_runtime=args.runtime,
20b2b6
            build_wheel=args.wheel,
20b2b6
            wheeldir=args.wheeldir,
7ef706
            toxenv=args.toxenv,
7ef706
            extras=args.extras,
7ef706
            generate_extras=args.generate_extras,
7ef706
            python3_pkgversion=args.python3_pkgversion,
7ef706
            requirement_files=args.requirement_files,
7ef706
            use_build_system=args.use_build_system,
7ef706
        )
7ef706
    except Exception:
7ef706
        # Log the traceback explicitly (it's useful debug info)
7ef706
        traceback.print_exc()
7ef706
        exit(1)
7ef706
7ef706
7ef706
if __name__ == '__main__':
7ef706
    main(sys.argv[1:])