# -*- coding: utf-8 -*-
"""Back-port compiler for Python 3.6 f-string literals."""
import argparse
import os
import pathlib
import re
import sys
import traceback
from typing import Generator, List, Optional, Union
import parso.python.tree
import parso.tree
import tbtrim
from bpc_utils import (BaseContext, BPCSyntaxError, Config, Placeholder, StringInterpolation,
TaskLock, archive_files, detect_encoding, detect_files, detect_indentation,
detect_linesep, first_non_none, get_parso_grammar_versions, map_tasks,
parse_boolean_state, parse_indentation, parse_linesep,
parse_positive_integer, parso_parse, recover_files)
from bpc_utils.typing import Linesep
from typing_extensions import ClassVar, Final, Literal, final
__all__ = ['main', 'f2format', 'convert'] # pylint: disable=undefined-all-variable
# version string
__version__ = '0.8.7rc1'
###############################################################################
# Typings
class F2formatConfig(Config):
indentation = '' # type: str
linesep = '\n' # type: Literal[Linesep]
pep8 = True # type: bool
filename = None # Optional[str]
source_version = None # Optional[str]
##############################################################################
# Auxiliaries
#: Get supported source versions.
#:
#: .. seealso:: :func:`bpc_utils.get_parso_grammar_versions`
F2FORMAT_SOURCE_VERSIONS = get_parso_grammar_versions(minimum='3.6')
# option default values
#: Default value for the ``quiet`` option.
_default_quiet = False
#: Default value for the ``concurrency`` option.
_default_concurrency = None # auto detect
#: Default value for the ``do_archive`` option.
_default_do_archive = True
#: Default value for the ``archive_path`` option.
_default_archive_path = 'archive'
#: Default value for the ``source_version`` option.
_default_source_version = F2FORMAT_SOURCE_VERSIONS[-1]
#: Default value for the ``linesep`` option.
_default_linesep = None # auto detect
#: Default value for the ``indentation`` option.
_default_indentation = None # auto detect
#: Default value for the ``pep8`` option.
_default_pep8 = True
# option getter utility functions
# option value precedence is: explicit value (CLI/API arguments) > environment variable > default value
[docs]def _get_quiet_option(explicit: Optional[bool] = None) -> Optional[bool]:
"""Get the value for the ``quiet`` option.
Args:
explicit (Optional[bool]): the value explicitly specified by user,
:data:`None` if not specified
Returns:
bool: the value for the ``quiet`` option
:Environment Variables:
:envvar:`F2FORMAT_QUIET` -- the value in environment variable
See Also:
:data:`_default_quiet`
"""
# We need short circuit evaluation, so first_non_none(a, b, c) does not work here
# with PEP 505 we can simply write a ?? b ?? c
def _option_layers() -> Generator[Optional[bool], None, None]:
yield explicit
yield parse_boolean_state(os.getenv('F2FORMAT_QUIET'))
yield _default_quiet
return first_non_none(_option_layers())
[docs]def _get_concurrency_option(explicit: Optional[int] = None) -> Optional[int]:
"""Get the value for the ``concurrency`` option.
Args:
explicit (Optional[int]): the value explicitly specified by user,
:data:`None` if not specified
Returns:
Optional[int]: the value for the ``concurrency`` option;
:data:`None` means *auto detection* at runtime
:Environment Variables:
:envvar:`F2FORMAT_CONCURRENCY` -- the value in environment variable
See Also:
:data:`_default_concurrency`
"""
return parse_positive_integer(explicit or os.getenv('F2FORMAT_CONCURRENCY') or _default_concurrency)
[docs]def _get_do_archive_option(explicit: Optional[bool] = None) -> Optional[bool]:
"""Get the value for the ``do_archive`` option.
Args:
explicit (Optional[bool]): the value explicitly specified by user,
:data:`None` if not specified
Returns:
bool: the value for the ``do_archive`` option
:Environment Variables:
:envvar:`F2FORMAT_DO_ARCHIVE` -- the value in environment variable
See Also:
:data:`_default_do_archive`
"""
def _option_layers() -> Generator[Optional[bool], None, None]:
yield explicit
yield parse_boolean_state(os.getenv('F2FORMAT_DO_ARCHIVE'))
yield _default_do_archive
return first_non_none(_option_layers())
[docs]def _get_archive_path_option(explicit: Optional[str] = None) -> str:
"""Get the value for the ``archive_path`` option.
Args:
explicit (Optional[str]): the value explicitly specified by user,
:data:`None` if not specified
Returns:
str: the value for the ``archive_path`` option
:Environment Variables:
:envvar:`F2FORMAT_ARCHIVE_PATH` -- the value in environment variable
See Also:
:data:`_default_archive_path`
"""
return explicit or os.getenv('F2FORMAT_ARCHIVE_PATH') or _default_archive_path
[docs]def _get_source_version_option(explicit: Optional[str] = None) -> Optional[str]:
"""Get the value for the ``source_version`` option.
Args:
explicit (Optional[str]): the value explicitly specified by user,
:data:`None` if not specified
Returns:
str: the value for the ``source_version`` option
:Environment Variables:
:envvar:`F2FORMAT_SOURCE_VERSION` -- the value in environment variable
See Also:
:data:`_default_source_version`
"""
return explicit or os.getenv('F2FORMAT_SOURCE_VERSION') or _default_source_version
[docs]def _get_linesep_option(explicit: Optional[str] = None) -> Optional[Linesep]:
r"""Get the value for the ``linesep`` option.
Args:
explicit (Optional[str]): the value explicitly specified by user,
:data:`None` if not specified
Returns:
Optional[Literal['\\n', '\\r\\n', '\\r']]: the value for the ``linesep`` option;
:data:`None` means *auto detection* at runtime
:Environment Variables:
:envvar:`F2FORMAT_LINESEP` -- the value in environment variable
See Also:
:data:`_default_linesep`
"""
return parse_linesep(explicit or os.getenv('F2FORMAT_LINESEP') or _default_linesep)
[docs]def _get_indentation_option(explicit: Optional[Union[str, int]] = None) -> Optional[str]:
"""Get the value for the ``indentation`` option.
Args:
explicit (Optional[Union[str, int]]): the value explicitly specified by user,
:data:`None` if not specified
Returns:
Optional[str]: the value for the ``indentation`` option;
:data:`None` means *auto detection* at runtime
:Environment Variables:
:envvar:`F2FORMAT_INDENTATION` -- the value in environment variable
See Also:
:data:`_default_indentation`
"""
return parse_indentation(explicit or os.getenv('F2FORMAT_INDENTATION') or _default_indentation)
[docs]def _get_pep8_option(explicit: Optional[bool] = None) -> Optional[bool]:
"""Get the value for the ``pep8`` option.
Args:
explicit (Optional[bool]): the value explicitly specified by user,
:data:`None` if not specified
Returns:
bool: the value for the ``pep8`` option
:Environment Variables:
:envvar:`F2FORMAT_PEP8` -- the value in environment variable
See Also:
:data:`_default_pep8`
"""
def _option_layers() -> Generator[Optional[bool], None, None]:
yield explicit
yield parse_boolean_state(os.getenv('F2FORMAT_PEP8'))
yield _default_pep8
return first_non_none(_option_layers())
###############################################################################
# Traceback Trimming (tbtrim)
# root path
ROOT = pathlib.Path(__file__).resolve().parent
def predicate(filename: str) -> bool:
return pathlib.Path(filename).parent == ROOT
tbtrim.set_trim_rule(predicate, strict=True, target=BPCSyntaxError)
###############################################################################
# Main convertion implementation
[docs]class Context(BaseContext):
"""General conversion context.
Args:
node (parso.tree.NodeOrLeaf): parso AST
config (Config): conversion configurations
Keyword Args:
raw (bool): raw processing flag
Important:
``raw`` should be :data:`True` only if the ``node`` is in the clause of another *context*,
where the converted wrapper functions should be inserted.
For the :class:`Context` class of :mod:`f2format` module,
it will process nodes with following methods:
* :token:`stringliteral`
* :meth:`Context._process_strings`
* :meth:`Context._process_string_context`
* :token:`f_string`
* :meth:`Context._process_fstring`
"""
[docs] def _process_strings(self, node: parso.python.tree.PythonNode) -> None:
"""Process concatenable strings (:token:`stringliteral`).
Args:
node (parso.python.tree.PythonNode): concatentable strings node
As in Python, adjacent string literals can be concatenated in certain
cases, as described in the `documentation`_. Such concatenable strings
may contain formatted string literals (:term:`f-string`) within its scope.
.. _documentation: https://docs.python.org/3/reference/lexical_analysis.html#string-literal-concatenation
"""
if not self.has_expr(node):
self += node.get_code()
return
# initialise new context
ctx = StringContext(node, self.config, indent_level=self._indent_level, raw=False) # type: ignore[arg-type]
self += ctx.string
[docs] def _process_fstring(self, node: parso.python.tree.PythonNode) -> None:
"""Process formatted strings (:token:`f_string`).
Args:
node (parso.python.tree.PythonNode): formatted strings node
"""
# initialise new context
ctx = StringContext(node, self.config, indent_level=self._indent_level, raw=False) # type: ignore[arg-type]
self += ctx.string
[docs] def _concat(self) -> None:
"""Concatenate final string.
This method tries to concatenate final result based on the very location
where starts to contain formatted string literals, i.e. between the converted
code as :attr:`self._prefix <Context._prefix>` and :attr:`self._suffix <Context._suffix>`.
"""
# no-op
self._buffer = self._prefix + self._suffix
[docs] @final
@classmethod
def has_expr(cls, node: parso.tree.NodeOrLeaf) -> bool:
"""Check if node has formatted string literals.
Args:
node (parso.tree.NodeOrLeaf): parso AST
Returns:
bool: if ``node`` has formatted string literals
"""
if node.type.startswith('fstring'):
return True
if hasattr(node, 'children'):
for child in node.children: # type: ignore[attr-defined]
if cls.has_expr(child):
return True
return False
# backward compatibility and auxiliary alias
has_f2format = has_expr
[docs] @final
@classmethod
def has_fstring(cls, node: parso.tree.NodeOrLeaf) -> bool:
"""Check if node has actual formatted string literals.
Args:
node (parso.tree.NodeOrLeaf): parso AST
Returns:
bool: if ``node`` has actual formatted string literals
(with expressions in the literals)
"""
if node.type == 'fstring_expr':
return True
if hasattr(node, 'children'):
for child in node.children: # type: ignore[attr-defined]
if cls.has_fstring(child):
return True
return False
[docs] @final
@classmethod
def has_debug_fstring(cls, node: parso.tree.NodeOrLeaf) -> bool:
"""Check if node has *debug* formatted string literals.
Args:
node (parso.tree.NodeOrLeaf): parso AST
Returns:
bool: if ``node`` has debug formatted string literals
"""
if node.type == 'fstring_expr':
return cls.is_debug_fstring(node) # type: ignore[arg-type]
if hasattr(node, 'children'):
for child in node.children: # type: ignore[attr-defined]
if cls.has_debug_fstring(child):
return True
return False
[docs] @final
@staticmethod
def is_debug_fstring(node: parso.python.tree.PythonNode) -> bool:
"""Check if node **is** *debug* formatted string literal expression (:token:`f_expression`).
Args:
node (parso.python.tree.PythonNode): formatted literal expression node
Returns:
bool: if ``node`` **is** debug formatted string literals
"""
if node.type != 'fstring_expr':
return False
for expr in node.children:
if expr.type == 'operator' and expr.value == '=':
next_sibling = expr.get_next_sibling()
if (next_sibling.type == 'operator' and next_sibling.value == '}'):
return True
if next_sibling.type in ['fstring_conversion', 'fstring_format_spec']:
return True
if next_sibling.type == 'operator' and next_sibling.value == ':':
next_next_sibling = next_sibling.get_next_sibling()
if next_next_sibling.type == 'operator' and next_next_sibling.value == '}':
return True
return False
[docs]class StringContext(Context):
"""String (f-string) conversion context.
This class is mainly used for converting **formatted string literals**.
Args:
node (parso.python.tree.PythonNode): parso AST
config (Config): conversion configurations
Keyword Args:
has_fstring (bool): flag if contains actual formatted
string literals (with expressions)
indent_level (int): current indentation level
raw (bool): raw processing flag
"""
#: re.Pattern: Pattern matches the formatted string literal prefix (``f``).
fstring_start = re.compile(r'[fF]', flags=re.ASCII) # type: Final[ClassVar[re.Pattern]]
#: re.Pattern: Pattern matches single brackets in the formatted string literal (``{}``).
fstring_bracket = re.compile(r'([{}])', flags=re.ASCII) # type: Final[ClassVar[re.Pattern]]
@final
@property
def expr(self) -> List[str]:
"""Expressions extracted from the formatted string literal.
:rtype: List[str]
"""
return self._expr
def __init__(self, node: parso.tree.NodeOrLeaf, config: F2formatConfig, *,
has_fstring: Optional[bool] = None, indent_level: int = 0, raw: bool = False):
if has_fstring is None:
has_fstring = self.has_fstring(node)
#: List[str]: Expressions extracted from the formatted string literal.
self._expr = [] # type: List[str]
#: bool: Flag if contains actual formatted string literals (with expressions).
self._flag = has_fstring # type: bool
# call super init
super().__init__(node, config, indent_level=indent_level, raw=raw)
[docs] def _process_fstring(self, node: parso.python.tree.PythonNode) -> None:
"""Process formatted strings (:token:`f_string`).
Args:
node (parso.python.tree.PythonNode): formatted strings node
"""
# initialise new context
ctx = StringContext(node, self.config, has_fstring=self._flag, # type: ignore[arg-type]
indent_level=self._indent_level, raw=True)
self += ctx.string
self._expr.extend(ctx.expr)
[docs] def _process_string(self, node: parso.python.tree.PythonNode) -> None:
"""Process string node (:token:`stringliteral`).
Args:
node (parso.python.tree.PythonNode): string node
"""
if self._flag:
self += self.fstring_bracket.sub(r'\1\1', node.get_code())
return
self += node.get_code()
[docs] def _process_fstring_start(self, node: parso.python.tree.FStringStart) -> None:
"""Process formatted string literal starting node (:token:`stringprefix`).
Args:
node (parso.python.tree.FStringStart): formatted literal starting node
"""
# <FStringStart: ...>
self += self.fstring_start.sub('', node.get_code())
[docs] def _process_fstring_string(self, node: parso.python.tree.FStringString) -> None:
"""Process formatted string literal string node (:token:`stringliteral`).
Args:
node (parso.python.tree.FStringString): formatted string literal string node
"""
if self._flag:
self += node.get_code()
return
self += node.get_code().replace('{{', '{').replace('}}', '}')
[docs] def _process_fstring_expr(self, node: parso.python.tree.PythonNode) -> None:
"""Process formatted string literal expression node (:token:`f_expression`).
Args:
node (parso.python.tree.PythonNode): formatted literal expression node
"""
# <Operator: {>
self += node.children[0].get_code().rstrip()
flag_imp = False # implicit tuple, generator expression and/or yield expression
flag_dbg = self.is_debug_fstring(node) # is debug f-string?
conv_str = '' # f-string conversion
spec_str = '' # f-string format spec
# NOTE: we need to maintain two SI, one keeps track of the original expression
# string for debug f-string, one keeps track of *sanitised* f-string with slots
# for `format` call, whose value will then be maintained in another list - the
# whole design here is to convert multi-layered debug f-string in a linear way,
# i.e., no need to do reverse lookup of expressions, etc.
expr_dbg = StringInterpolation() # debug f-string original expression buffer
expr_str = StringInterpolation() # extracted expression buffer - string part
expr_fmt = [] # extracted expression buffer - format expression part
# testlist ['='] [ fstring_conversion ] [ fstring_format_spec ]
for child in node.children[1:-1]:
# conversion
if child.type == 'fstring_conversion':
temp = child.get_code().strip()
if flag_dbg:
conv_str += temp
else:
self += temp
# format specification
elif child.type == 'fstring_format_spec':
# initialise new context
ctx = StringContext(child, self.config, has_fstring=None, # type: ignore[arg-type]
indent_level=self._indent_level, raw=True)
temp = ctx.string.strip()
if flag_dbg:
conv_str += temp
else:
self += temp
spec_str += ''.join(ctx.expr)
# empty format specification
elif child.type == 'operator' and child.value == ':':
next_sibling = child.get_next_sibling()
if (next_sibling.type == 'operator' and next_sibling.value == '}'):
temp = child.get_code()
if flag_dbg:
conv_str += temp
else:
self += child.get_code()
else:
code = child.get_code()
expr_dbg += code
expr_str += code
# embedded f-string
elif child.type == 'fstring':
# initialise new context
ctx = StringContext(child, self.config, has_fstring=None, # type: ignore[arg-type]
indent_level=self._indent_level, raw=False)
if flag_dbg:
expr_dbg += self.fstring_bracket.sub(r'\1\1', child.get_code())
expr_str += ctx._prefix + ctx._suffix # pylint: disable=protected-access
expr_fmt.extend(ctx.expr)
else:
expr_str += ctx.string
# concatenable strings
elif child.type == 'strings':
# initialise new context
ctx = StringContext(child, self.config, has_fstring=None, # type: ignore[arg-type]
indent_level=self._indent_level, raw=False)
if flag_dbg:
expr_dbg += self.fstring_bracket.sub(r'\1\1', child.get_code())
expr_str += ctx._prefix + ctx._suffix # pylint: disable=protected-access
expr_fmt.extend(ctx.expr)
else:
expr_str += ctx.string
# debug f-string / normal expression
elif child.type == 'operator' and child.value == '=':
if flag_dbg:
next_sibling = child.get_next_sibling()
expr_dbg += child.get_code() + self.extract_whitespaces(next_sibling.get_code())[0] + \
'{' + Placeholder('conv_str') + '}'
if flag_imp:
expr_str = '(' + expr_str + ')'
if expr_fmt:
expr_str = Placeholder('expr_dbg') + '.format(' + expr_str + \
'.format(%s)' % ', '.join(map(lambda s: s.strip(), expr_fmt)) + ')'
else:
expr_str = Placeholder('expr_dbg') + '.format(' + expr_str + ')'
else:
expr_str += child.get_code()
# normal expression
else:
# structures which need a pair of parentheses when converted to str.format() calls:
# implicit tuple, generator expression and yield expression
if child.type in {'testlist', 'testlist_comp', 'yield_expr'} \
or child.type == 'keyword' and child.value == 'yield':
flag_imp = True
code = child.get_code()
expr_str += code
expr_dbg += code
if expr_str:
if flag_dbg:
expr_tmp = expr_dbg % {'conv_str': conv_str or '!r'}
expr_str = expr_str % {'expr_dbg': repr(expr_tmp.result)}
if flag_imp:
expr_str = '(' + expr_str + ')'
self._expr.append(expr_str.result)
if spec_str:
self._expr.append(spec_str)
# <Operator: }>
self += node.children[-1].get_code().lstrip()
[docs] def _concat(self) -> None:
"""Concatenate final string.
This method tries to concatenate final result based on the very location
where starts to contain formatted string literals, i.e. between the converted
code as :attr:`self._prefix <Context._prefix>` and :attr:`self._suffix <Context._suffix>`.
"""
if self._expr:
if self._pep8:
self._buffer = self._prefix + self._suffix.rstrip() + \
'.format(%s)' % ', '.join(map(lambda s: s.strip(), self._expr))
else:
self._buffer = self._prefix + self._suffix + '.format(%s)' % ', '.join(self._expr)
return
# no-op
self._buffer = self._prefix + self._suffix
###############################################################################
# Public Interface
[docs]def convert(code: Union[str, bytes], filename: Optional[str] = None, *,
source_version: Optional[str] = None, linesep: Optional[Linesep] = None,
indentation: Optional[Union[int, str]] = None, pep8: Optional[bool] = None) -> str:
"""Convert the given Python source code string.
Args:
code (Union[str, bytes]): the source code to be converted
filename (Optional[str]): an optional source file name to provide a context in case of error
Keyword Args:
source_version (Optional[str]): parse the code as this Python version (uses the latest version by default)
:Environment Variables:
- :envvar:`F2FORMAT_SOURCE_VERSION` -- same as the ``source_version`` argument and the ``--source-version`` option
in CLI
- :envvar:`F2FORMAT_LINESEP` -- same as the ``linesep`` argument and the ``--linesep`` option in CLI
- :envvar:`F2FORMAT_INDENTATION` -- same as the ``indentation`` argument and the ``--indentation`` option in CLI
- :envvar:`F2FORMAT_PEP8` -- same as the ``pep8`` argument and the ``--no-pep8`` option in CLI (logical negation)
Returns:
str: converted source code
"""
# parse source string
source_version = _get_source_version_option(source_version)
module = parso_parse(code, filename=filename, version=source_version)
# get linesep, indentation and pep8 options
linesep = _get_linesep_option(linesep)
indentation = _get_indentation_option(indentation)
if linesep is None:
linesep = detect_linesep(code)
if indentation is None:
indentation = detect_indentation(code)
pep8 = _get_pep8_option(pep8)
# pack conversion configuration
config = Config(linesep=linesep, indentation=indentation, pep8=pep8,
filename=filename, source_version=source_version)
# convert source string
result = Context(module, config).string
# return conversion result
return result
###############################################################################
# CLI & Entry Point
# option values display
# these values are only intended for argparse help messages
# this shows default values by default, environment variables may override them
__cwd__ = os.getcwd()
__f2format_quiet__ = 'quiet mode' if _get_quiet_option() else 'non-quiet mode'
__f2format_concurrency__ = _get_concurrency_option() or 'auto detect'
__f2format_do_archive__ = 'will do archive' if _get_do_archive_option() else 'will not do archive'
__f2format_archive_path__ = os.path.join(__cwd__, _get_archive_path_option())
__f2format_source_version__ = _get_source_version_option()
__f2format_linesep__ = {
'\n': 'LF',
'\r\n': 'CRLF',
'\r': 'CR',
None: 'auto detect'
}[_get_linesep_option()]
__f2format_indentation__ = _get_indentation_option()
if __f2format_indentation__ is None:
__f2format_indentation__ = 'auto detect'
elif __f2format_indentation__ == '\t':
__f2format_indentation__ = 'tab'
else:
__f2format_indentation__ = '%d spaces' % len(__f2format_indentation__)
__f2format_pep8__ = 'will conform to PEP 8' if _get_pep8_option() else 'will not conform to PEP 8'
[docs]def get_parser() -> argparse.ArgumentParser:
"""Generate CLI parser.
Returns:
argparse.ArgumentParser: CLI parser for f2format
"""
parser = argparse.ArgumentParser(prog='f2format',
usage='f2format [options] <Python source files and directories...>',
description='Back-port compiler for Python 3.8 position-only parameters.')
parser.add_argument('-V', '--version', action='version', version=__version__)
parser.add_argument('-q', '--quiet', action='store_true', default=None,
help='run in quiet mode (current: %s)' % __f2format_quiet__)
parser.add_argument('-C', '--concurrency', action='store', type=int, metavar='N',
help='the number of concurrent processes for conversion (current: %s)' % __f2format_concurrency__)
parser.add_argument('--dry-run', action='store_true',
help='list the files to be converted without actually performing conversion and archiving')
parser.add_argument('-s', '--simple', action='store', nargs='?', dest='simple_args', const='', metavar='FILE',
help='this option tells the program to operate in "simple mode"; '
'if a file name is provided, the program will convert the file but print conversion '
'result to standard output instead of overwriting the file; '
'if no file names are provided, read code for conversion from standard input and print '
'conversion result to standard output; '
'in "simple mode", no file names shall be provided via positional arguments')
archive_group = parser.add_argument_group(title='archive options',
description="backup original files in case there're any issues")
archive_group.add_argument('-na', '--no-archive', action='store_false', dest='do_archive', default=None,
help='do not archive original files (current: %s)' % __f2format_do_archive__)
archive_group.add_argument('-k', '--archive-path', action='store', default=__f2format_archive_path__, metavar='PATH', # pylint: disable=line-too-long
help='path to archive original files (current: %(default)s)')
archive_group.add_argument('-r', '--recover', action='store', dest='recover_file', metavar='ARCHIVE_FILE',
help='recover files from a given archive file')
# archive_group.add_argument('-r2', action='store_true', help='remove the archive file after recovery')
# archive_group.add_argument('-r3', action='store_true', help='remove the archive file after recovery, '
# 'and remove the archive directory if it becomes empty')
convert_group = parser.add_argument_group(title='convert options', description='conversion configuration')
convert_group.add_argument('-vs', '-vf', '--source-version', '--from-version', action='store', metavar='VERSION',
default=__f2format_source_version__, choices=F2FORMAT_SOURCE_VERSIONS,
help='parse source code as this Python version (current: %(default)s)')
convert_group.add_argument('-l', '--linesep', action='store',
help='line separator (LF, CRLF, CR) to read '
'source files (current: %s)' % __f2format_linesep__)
convert_group.add_argument('-t', '--indentation', action='store', metavar='INDENT',
help='code indentation style, specify an integer for the number of spaces, '
"or 't'/'tab' for tabs (current: %s)" % __f2format_indentation__)
convert_group.add_argument('-n8', '--no-pep8', action='store_false', dest='pep8', default=None,
help='do not make code insertion PEP 8 compliant (current: %s)' % __f2format_pep8__)
parser.add_argument('files', action='store', nargs='*', metavar='<Python source files and directories...>',
help='Python source files and directories to be converted')
return parser
def do_f2format(filename: str, **kwargs: object) -> None:
"""Wrapper function to catch exceptions."""
try:
f2format(filename, **kwargs) # type: ignore[arg-type]
except Exception: # pylint: disable=broad-except
with TaskLock():
print('Failed to convert file: %r' % filename, file=sys.stderr)
traceback.print_exc()
[docs]def main(argv: Optional[List[str]] =None) -> int:
"""Entry point for f2format.
Args:
argv (Optional[List[str]]): CLI arguments
:Environment Variables:
- :envvar:`F2FORMAT_QUIET` -- same as the ``--quiet`` option in CLI
- :envvar:`F2FORMAT_CONCURRENCY` -- same as the ``--concurrency`` option in CLI
- :envvar:`F2FORMAT_DO_ARCHIVE` -- same as the ``--no-archive`` option in CLI (logical negation)
- :envvar:`F2FORMAT_ARCHIVE_PATH` -- same as the ``--archive-path`` option in CLI
- :envvar:`F2FORMAT_SOURCE_VERSION` -- same as the ``--source-version`` option in CLI
- :envvar:`F2FORMAT_LINESEP` -- same as the ``--linesep`` option in CLI
- :envvar:`F2FORMAT_INDENTATION` -- same as the ``--indentation`` option in CLI
- :envvar:`F2FORMAT_PEP8` -- same as the ``--no-pep8`` option in CLI (logical negation)
"""
parser = get_parser()
args = parser.parse_args(argv)
options = {
'source_version': args.source_version,
'linesep': args.linesep,
'indentation': args.indentation,
'pep8': args.pep8,
}
# check if running in simple mode
if args.simple_args is not None:
if args.files:
parser.error('no Python source files or directories shall be given as positional arguments in simple mode')
if not args.simple_args: # read from stdin
code = sys.stdin.read()
else: # read from file
filename = args.simple_args
options['filename'] = filename
with open(filename, 'rb') as file:
code = file.read()
sys.stdout.write(convert(code, **options)) # print conversion result to stdout
return 0
# get options
quiet = _get_quiet_option(args.quiet)
processes = _get_concurrency_option(args.concurrency)
do_archive = _get_do_archive_option(args.do_archive)
archive_path = _get_archive_path_option(args.archive_path)
# check if doing recovery
if args.recover_file:
recover_files(args.recover_file)
if not args.quiet:
print('Recovered files from archive: %r' % args.recover_file, file=sys.stderr)
# TODO: maybe implement deletion in bpc-utils?
# if args.r2 or args.r3:
# os.remove(args.recover_file)
# if args.r3:
# archive_dir = os.path.dirname(os.path.realpath(args.recover_file))
# if not os.listdir(archive_dir):
# os.rmdir(archive_dir)
return 0
# fetch file list
if not args.files:
parser.error('no Python source files or directories are given')
filelist = sorted(detect_files(args.files))
# terminate if no valid Python source files detected
if not filelist:
if not args.quiet:
# TODO: maybe use parser.error?
print('Warning: no valid Python source files found in %r' % (args.files,), file=sys.stderr)
return 1
# make archive
if do_archive and not args.dry_run:
archive_files(filelist, archive_path)
# process files
options.update({
'quiet': quiet,
'dry_run': args.dry_run,
})
map_tasks(do_f2format, filelist, kwargs=options, processes=processes)
return 0
if __name__ == '__main__':
sys.exit(main())