return all(feature in VERSION_TO_FEATURES[version] for version in target_versions)
-def find_pyproject_toml(path_search_start: str) -> Optional[str]:
+def find_pyproject_toml(path_search_start: Iterable[str]) -> Optional[str]:
"""Find the absolute filepath to a pyproject.toml if it exists"""
path_project_root = find_project_root(path_search_start)
path_pyproject_toml = path_project_root / "pyproject.toml"
if not config:
return None
+ else:
+ # Sanitize the values to be Click friendly. For more information please see:
+ # https://github.com/psf/black/issues/1458
+ # https://github.com/pallets/click/issues/1567
+ config = {
+ k: str(v) if not isinstance(v, (list, dict)) else v
+ for k, v in config.items()
+ }
target_version = config.get("target_version")
if target_version is not None and not isinstance(target_version, list):
raise click.BadOptionUsage(
- "target-version", f"Config key target-version must be a list"
+ "target-version", "Config key target-version must be a list"
)
default_map: Dict[str, Any] = {}
),
is_eager=True,
callback=read_pyproject_toml,
- help="Read configuration from PATH.",
+ help="Read configuration from FILE path.",
)
@click.pass_context
def main(
worker_count = min(worker_count, 61)
try:
executor = ProcessPoolExecutor(max_workers=worker_count)
- except OSError:
+ except (ImportError, OSError):
# we arrive here if the underlying system does not support multi-processing
- # like in AWS Lambda, in which case we gracefully fallback to
+ # like in AWS Lambda or Termux, in which case we gracefully fallback to
# a ThreadPollExecutor with just a single worker (more workers would not do us
# any good due to the Global Interpreter Lock)
executor = ThreadPoolExecutor(max_workers=1)
# one line in the original code.
# Grab the first and last line numbers, skipping generated leaves
- first_line = next((l.lineno for l in self.leaves if l.lineno != 0), 0)
- last_line = next((l.lineno for l in reversed(self.leaves) if l.lineno != 0), 0)
+ first_line = next((leaf.lineno for leaf in self.leaves if leaf.lineno != 0), 0)
+ last_line = next(
+ (leaf.lineno for leaf in reversed(self.leaves) if leaf.lineno != 0), 0
+ )
if first_line == last_line:
# We look at the last two leaves since a comma or an
# split altogether.
result: List[Line] = []
try:
- for l in transform(line, features):
- if str(l).strip("\n") == line_str:
+ for transformed_line in transform(line, features):
+ if str(transformed_line).strip("\n") == line_str:
raise CannotTransform(
"Line transformer returned an unchanged result"
)
result.extend(
transform_line(
- l,
+ transformed_line,
line_length=line_length,
normalize_strings=normalize_strings,
features=features,
StringTransformer to be applied?
Transformations:
- If the given Line meets all of the above requirments, which string
+ If the given Line meets all of the above requirements, which string
transformations can you expect to be applied to it by this
StringTransformer?
# We MAY choose to drop the 'f' prefix from substrings that don't
# contain any f-expressions, but ONLY if the original f-string
- # containes at least one f-expression. Otherwise, we will alter the AST
+ # contains at least one f-expression. Otherwise, we will alter the AST
# of the program.
drop_pointless_f_prefix = ("f" in prefix) and re.search(
self.RE_FEXPR, LL[string_idx].value, re.VERBOSE
max_bidx = max_break_idx - 2 if line_needs_plus() else max_break_idx
maybe_break_idx = self.__get_break_idx(rest_value, max_bidx)
if maybe_break_idx is None:
- # If we are unable to algorthmically determine a good split
+ # If we are unable to algorithmically determine a good split
# and this string has custom splits registered to it, we
# fall back to using them--which means we have to start
# over from the beginning.
class StringParser:
"""
A state machine that aids in parsing a string's "trailer", which can be
- either non-existant, an old-style formatting sequence (e.g. `% varX` or `%
+ either non-existent, an old-style formatting sequence (e.g. `% varX` or `%
(varX, varY)`), or a method-call / attribute access (e.g. `.format(varX,
varY)`).
was directly after the string leaf in question (e.g. if our target
string is `line.leaves[i]` then the first call to this method must
be `line.leaves[i + 1]`).
- * On the next call to this function, the leaf paramater passed in
+ * On the next call to this function, the leaf parameter passed in
MUST be the leaf directly following @leaf.
Returns:
All of the leaves in @leaves are duplicated. The duplicates are then
appended to @new_line and used to replace their originals in the underlying
- Node structure. Any comments attatched to the old leaves are reattached to
+ Node structure. Any comments attached to the old leaves are reattached to
the new leaves.
Pre-conditions:
no_commas = (
original.is_def
and opening_bracket.value == "("
- and not any(l.type == token.COMMA for l in leaves)
+ and not any(leaf.type == token.COMMA for leaf in leaves)
)
if original.is_import or no_commas:
@wraps(split_func)
def split_wrapper(line: Line, features: Collection[Feature] = ()) -> Iterator[Line]:
- for l in split_func(line, features):
- normalize_prefix(l.leaves[0], inside_brackets=True)
- yield l
+ for line in split_func(line, features):
+ normalize_prefix(line.leaves[0], inside_brackets=True)
+ yield line
return split_wrapper
def find_project_root(srcs: Iterable[str]) -> Path:
"""Return a directory containing .git, .hg, or pyproject.toml.
- That directory can be one of the directories passed in `srcs` or their
- common parent.
+ That directory will be a common parent of all files and directories
+ passed in `srcs`.
If no directory in the tree contains a marker that would specify it's the
project root, the root of the file system is returned.
if not srcs:
return Path("/").resolve()
- common_base = min(Path(src).resolve() for src in srcs)
- if common_base.is_dir():
- # Append a fake file so `parents` below returns `common_base_dir`, too.
- common_base /= "fake-file"
- for directory in common_base.parents:
+ path_srcs = [Path(src).resolve() for src in srcs]
+
+ # A list of lists of parents for each 'src'. 'src' is included as a
+ # "parent" of itself if it is a directory
+ src_parents = [
+ list(path.parents) + ([path] if path.is_dir() else []) for path in path_srcs
+ ]
+
+ common_base = max(
+ set.intersection(*(set(parents) for parents in src_parents)),
+ key=lambda path: path.parts,
+ )
+
+ for directory in (common_base, *common_base.parents):
if (directory / ".git").exists():
return directory
else:
# Constant strings may be indented across newlines, if they are
- # docstrings; fold spaces after newlines when comparing
+ # docstrings; fold spaces after newlines when comparing. Similarly,
+ # trailing and leading space may be removed.
if (
isinstance(node, ast.Constant)
and field == "value"
and isinstance(value, str)
):
- normalized = re.sub(r"\n[ \t]+", "\n ", value)
+ normalized = re.sub(r" *\n[ \t]+", "\n ", value).strip()
else:
normalized = value
yield f"{' ' * (depth+2)}{normalized!r}, # {value.__class__.__name__}"