Refactor code structure for improved readability and maintainability

This commit is contained in:
claudi 2026-04-07 09:10:53 +02:00
parent 389d72a136
commit aa4c067ea8
1685 changed files with 393439 additions and 71932 deletions

View file

@ -0,0 +1 @@
version = "26.3.1"

View file

@ -0,0 +1 @@
version: str

View file

@ -0,0 +1,128 @@
import sys
import os
import re
import importlib
import warnings
is_pypy = '__pypy__' in sys.builtin_module_names
warnings.filterwarnings('ignore',
r'.+ distutils\b.+ deprecated',
DeprecationWarning)
def warn_distutils_present():
if 'distutils' not in sys.modules:
return
if is_pypy and sys.version_info < (3, 7):
# PyPy for 3.6 unconditionally imports distutils, so bypass the warning
# https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
return
warnings.warn(
"Distutils was imported before Setuptools, but importing Setuptools "
"also replaces the `distutils` module in `sys.modules`. This may lead "
"to undesirable behaviors or errors. To avoid these issues, avoid "
"using distutils directly, ensure that setuptools is installed in the "
"traditional way (e.g. not an editable install), and/or make sure "
"that setuptools is always imported before distutils.")
def clear_distutils():
if 'distutils' not in sys.modules:
return
warnings.warn("Setuptools is replacing distutils.")
mods = [name for name in sys.modules if re.match(r'distutils\b', name)]
for name in mods:
del sys.modules[name]
def enabled():
"""
Allow selection of distutils by environment variable.
"""
which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'stdlib')
return which == 'local'
def ensure_local_distutils():
clear_distutils()
distutils = importlib.import_module('setuptools._distutils')
distutils.__name__ = 'distutils'
sys.modules['distutils'] = distutils
# sanity check that submodules load as expected
core = importlib.import_module('distutils.core')
assert '_distutils' in core.__file__, core.__file__
def do_override():
"""
Ensure that the local copy of distutils is preferred over stdlib.
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
for more motivation.
"""
if enabled():
warn_distutils_present()
ensure_local_distutils()
class DistutilsMetaFinder:
def find_spec(self, fullname, path, target=None):
if path is not None:
return
method_name = 'spec_for_{fullname}'.format(**locals())
method = getattr(self, method_name, lambda: None)
return method()
def spec_for_distutils(self):
import importlib.abc
import importlib.util
class DistutilsLoader(importlib.abc.Loader):
def create_module(self, spec):
return importlib.import_module('setuptools._distutils')
def exec_module(self, module):
pass
return importlib.util.spec_from_loader('distutils', DistutilsLoader())
def spec_for_pip(self):
"""
Ensure stdlib distutils when running under pip.
See pypa/pip#8761 for rationale.
"""
if self.pip_imported_during_build():
return
clear_distutils()
self.spec_for_distutils = lambda: None
@staticmethod
def pip_imported_during_build():
"""
Detect if pip is being imported in a build script. Ref #2355.
"""
import traceback
return any(
frame.f_globals['__file__'].endswith('setup.py')
for frame, line in traceback.walk_stack(None)
)
DISTUTILS_FINDER = DistutilsMetaFinder()
def add_shim():
sys.meta_path.insert(0, DISTUTILS_FINDER)
def remove_shim():
try:
sys.meta_path.remove(DISTUTILS_FINDER)
except ValueError:
pass

View file

@ -0,0 +1 @@
__import__('_distutils_hack').do_override()

View file

@ -0,0 +1,33 @@
# This is a stub package designed to roughly emulate the _yaml
# extension module, which previously existed as a standalone module
# and has been moved into the `yaml` package namespace.
# It does not perfectly mimic its old counterpart, but should get
# close enough for anyone who's relying on it even when they shouldn't.
import yaml
# in some circumstances, the yaml module we imoprted may be from a different version, so we need
# to tread carefully when poking at it here (it may not have the attributes we expect)
if not getattr(yaml, '__with_libyaml__', False):
from sys import version_info
exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
raise exc("No module named '_yaml'")
else:
from yaml._yaml import *
import warnings
warnings.warn(
'The _yaml extension module is now located at yaml._yaml'
' and its location is subject to change. To use the'
' LibYAML-based parser and emitter, import from `yaml`:'
' `from yaml import CLoader as Loader, CDumper as Dumper`.',
DeprecationWarning
)
del warnings
# Don't `del yaml` here because yaml is actually an existing
# namespace member of _yaml.
__name__ = '_yaml'
# If the module is top-level (i.e. not a part of any specific package)
# then the attribute should be set to ''.
# https://docs.python.org/3.8/library/types.html
__package__ = ''

View file

@ -0,0 +1,295 @@
Metadata-Version: 2.3
Name: annotated-types
Version: 0.7.0
Summary: Reusable constraint types to use with typing.Annotated
Project-URL: Homepage, https://github.com/annotated-types/annotated-types
Project-URL: Source, https://github.com/annotated-types/annotated-types
Project-URL: Changelog, https://github.com/annotated-types/annotated-types/releases
Author-email: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Samuel Colvin <s@muelcolvin.com>, Zac Hatfield-Dodds <zac@zhd.dev>
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Environment :: MacOS X
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: Unix
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8
Requires-Dist: typing-extensions>=4.0.0; python_version < '3.9'
Description-Content-Type: text/markdown
# annotated-types
[![CI](https://github.com/annotated-types/annotated-types/workflows/CI/badge.svg?event=push)](https://github.com/annotated-types/annotated-types/actions?query=event%3Apush+branch%3Amain+workflow%3ACI)
[![pypi](https://img.shields.io/pypi/v/annotated-types.svg)](https://pypi.python.org/pypi/annotated-types)
[![versions](https://img.shields.io/pypi/pyversions/annotated-types.svg)](https://github.com/annotated-types/annotated-types)
[![license](https://img.shields.io/github/license/annotated-types/annotated-types.svg)](https://github.com/annotated-types/annotated-types/blob/main/LICENSE)
[PEP-593](https://peps.python.org/pep-0593/) added `typing.Annotated` as a way of
adding context-specific metadata to existing types, and specifies that
`Annotated[T, x]` _should_ be treated as `T` by any tool or library without special
logic for `x`.
This package provides metadata objects which can be used to represent common
constraints such as upper and lower bounds on scalar values and collection sizes,
a `Predicate` marker for runtime checks, and
descriptions of how we intend these metadata to be interpreted. In some cases,
we also note alternative representations which do not require this package.
## Install
```bash
pip install annotated-types
```
## Examples
```python
from typing import Annotated
from annotated_types import Gt, Len, Predicate
class MyClass:
age: Annotated[int, Gt(18)] # Valid: 19, 20, ...
# Invalid: 17, 18, "19", 19.0, ...
factors: list[Annotated[int, Predicate(is_prime)]] # Valid: 2, 3, 5, 7, 11, ...
# Invalid: 4, 8, -2, 5.0, "prime", ...
my_list: Annotated[list[int], Len(0, 10)] # Valid: [], [10, 20, 30, 40, 50]
# Invalid: (1, 2), ["abc"], [0] * 20
```
## Documentation
_While `annotated-types` avoids runtime checks for performance, users should not
construct invalid combinations such as `MultipleOf("non-numeric")` or `Annotated[int, Len(3)]`.
Downstream implementors may choose to raise an error, emit a warning, silently ignore
a metadata item, etc., if the metadata objects described below are used with an
incompatible type - or for any other reason!_
### Gt, Ge, Lt, Le
Express inclusive and/or exclusive bounds on orderable values - which may be numbers,
dates, times, strings, sets, etc. Note that the boundary value need not be of the
same type that was annotated, so long as they can be compared: `Annotated[int, Gt(1.5)]`
is fine, for example, and implies that the value is an integer x such that `x > 1.5`.
We suggest that implementors may also interpret `functools.partial(operator.le, 1.5)`
as being equivalent to `Gt(1.5)`, for users who wish to avoid a runtime dependency on
the `annotated-types` package.
To be explicit, these types have the following meanings:
* `Gt(x)` - value must be "Greater Than" `x` - equivalent to exclusive minimum
* `Ge(x)` - value must be "Greater than or Equal" to `x` - equivalent to inclusive minimum
* `Lt(x)` - value must be "Less Than" `x` - equivalent to exclusive maximum
* `Le(x)` - value must be "Less than or Equal" to `x` - equivalent to inclusive maximum
### Interval
`Interval(gt, ge, lt, le)` allows you to specify an upper and lower bound with a single
metadata object. `None` attributes should be ignored, and non-`None` attributes
treated as per the single bounds above.
### MultipleOf
`MultipleOf(multiple_of=x)` might be interpreted in two ways:
1. Python semantics, implying `value % multiple_of == 0`, or
2. [JSONschema semantics](https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.2.1),
where `int(value / multiple_of) == value / multiple_of`.
We encourage users to be aware of these two common interpretations and their
distinct behaviours, especially since very large or non-integer numbers make
it easy to cause silent data corruption due to floating-point imprecision.
We encourage libraries to carefully document which interpretation they implement.
### MinLen, MaxLen, Len
`Len()` implies that `min_length <= len(value) <= max_length` - lower and upper bounds are inclusive.
As well as `Len()` which can optionally include upper and lower bounds, we also
provide `MinLen(x)` and `MaxLen(y)` which are equivalent to `Len(min_length=x)`
and `Len(max_length=y)` respectively.
`Len`, `MinLen`, and `MaxLen` may be used with any type which supports `len(value)`.
Examples of usage:
* `Annotated[list, MaxLen(10)]` (or `Annotated[list, Len(max_length=10))`) - list must have a length of 10 or less
* `Annotated[str, MaxLen(10)]` - string must have a length of 10 or less
* `Annotated[list, MinLen(3))` (or `Annotated[list, Len(min_length=3))`) - list must have a length of 3 or more
* `Annotated[list, Len(4, 6)]` - list must have a length of 4, 5, or 6
* `Annotated[list, Len(8, 8)]` - list must have a length of exactly 8
#### Changed in v0.4.0
* `min_inclusive` has been renamed to `min_length`, no change in meaning
* `max_exclusive` has been renamed to `max_length`, upper bound is now **inclusive** instead of **exclusive**
* The recommendation that slices are interpreted as `Len` has been removed due to ambiguity and different semantic
meaning of the upper bound in slices vs. `Len`
See [issue #23](https://github.com/annotated-types/annotated-types/issues/23) for discussion.
### Timezone
`Timezone` can be used with a `datetime` or a `time` to express which timezones
are allowed. `Annotated[datetime, Timezone(None)]` must be a naive datetime.
`Timezone[...]` ([literal ellipsis](https://docs.python.org/3/library/constants.html#Ellipsis))
expresses that any timezone-aware datetime is allowed. You may also pass a specific
timezone string or [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects)
object such as `Timezone(timezone.utc)` or `Timezone("Africa/Abidjan")` to express that you only
allow a specific timezone, though we note that this is often a symptom of fragile design.
#### Changed in v0.x.x
* `Timezone` accepts [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects) objects instead of
`timezone`, extending compatibility to [`zoneinfo`](https://docs.python.org/3/library/zoneinfo.html) and third party libraries.
### Unit
`Unit(unit: str)` expresses that the annotated numeric value is the magnitude of
a quantity with the specified unit. For example, `Annotated[float, Unit("m/s")]`
would be a float representing a velocity in meters per second.
Please note that `annotated_types` itself makes no attempt to parse or validate
the unit string in any way. That is left entirely to downstream libraries,
such as [`pint`](https://pint.readthedocs.io) or
[`astropy.units`](https://docs.astropy.org/en/stable/units/).
An example of how a library might use this metadata:
```python
from annotated_types import Unit
from typing import Annotated, TypeVar, Callable, Any, get_origin, get_args
# given a type annotated with a unit:
Meters = Annotated[float, Unit("m")]
# you can cast the annotation to a specific unit type with any
# callable that accepts a string and returns the desired type
T = TypeVar("T")
def cast_unit(tp: Any, unit_cls: Callable[[str], T]) -> T | None:
if get_origin(tp) is Annotated:
for arg in get_args(tp):
if isinstance(arg, Unit):
return unit_cls(arg.unit)
return None
# using `pint`
import pint
pint_unit = cast_unit(Meters, pint.Unit)
# using `astropy.units`
import astropy.units as u
astropy_unit = cast_unit(Meters, u.Unit)
```
### Predicate
`Predicate(func: Callable)` expresses that `func(value)` is truthy for valid values.
Users should prefer the statically inspectable metadata above, but if you need
the full power and flexibility of arbitrary runtime predicates... here it is.
For some common constraints, we provide generic types:
* `IsLower = Annotated[T, Predicate(str.islower)]`
* `IsUpper = Annotated[T, Predicate(str.isupper)]`
* `IsDigit = Annotated[T, Predicate(str.isdigit)]`
* `IsFinite = Annotated[T, Predicate(math.isfinite)]`
* `IsNotFinite = Annotated[T, Predicate(Not(math.isfinite))]`
* `IsNan = Annotated[T, Predicate(math.isnan)]`
* `IsNotNan = Annotated[T, Predicate(Not(math.isnan))]`
* `IsInfinite = Annotated[T, Predicate(math.isinf)]`
* `IsNotInfinite = Annotated[T, Predicate(Not(math.isinf))]`
so that you can write e.g. `x: IsFinite[float] = 2.0` instead of the longer
(but exactly equivalent) `x: Annotated[float, Predicate(math.isfinite)] = 2.0`.
Some libraries might have special logic to handle known or understandable predicates,
for example by checking for `str.isdigit` and using its presence to both call custom
logic to enforce digit-only strings, and customise some generated external schema.
Users are therefore encouraged to avoid indirection like `lambda s: s.lower()`, in
favor of introspectable methods such as `str.lower` or `re.compile("pattern").search`.
To enable basic negation of commonly used predicates like `math.isnan` without introducing introspection that makes it impossible for implementers to introspect the predicate we provide a `Not` wrapper that simply negates the predicate in an introspectable manner. Several of the predicates listed above are created in this manner.
We do not specify what behaviour should be expected for predicates that raise
an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
skip invalid constraints, or statically raise an error; or it might try calling it
and then propagate or discard the resulting
`TypeError: descriptor 'isdigit' for 'str' objects doesn't apply to a 'int' object`
exception. We encourage libraries to document the behaviour they choose.
### Doc
`doc()` can be used to add documentation information in `Annotated`, for function and method parameters, variables, class attributes, return types, and any place where `Annotated` can be used.
It expects a value that can be statically analyzed, as the main use case is for static analysis, editors, documentation generators, and similar tools.
It returns a `DocInfo` class with a single attribute `documentation` containing the value passed to `doc()`.
This is the early adopter's alternative form of the [`typing-doc` proposal](https://github.com/tiangolo/fastapi/blob/typing-doc/typing_doc.md).
### Integrating downstream types with `GroupedMetadata`
Implementers may choose to provide a convenience wrapper that groups multiple pieces of metadata.
This can help reduce verbosity and cognitive overhead for users.
For example, an implementer like Pydantic might provide a `Field` or `Meta` type that accepts keyword arguments and transforms these into low-level metadata:
```python
from dataclasses import dataclass
from typing import Iterator
from annotated_types import GroupedMetadata, Ge
@dataclass
class Field(GroupedMetadata):
ge: int | None = None
description: str | None = None
def __iter__(self) -> Iterator[object]:
# Iterating over a GroupedMetadata object should yield annotated-types
# constraint metadata objects which describe it as fully as possible,
# and may include other unknown objects too.
if self.ge is not None:
yield Ge(self.ge)
if self.description is not None:
yield Description(self.description)
```
Libraries consuming annotated-types constraints should check for `GroupedMetadata` and unpack it by iterating over the object and treating the results as if they had been "unpacked" in the `Annotated` type. The same logic should be applied to the [PEP 646 `Unpack` type](https://peps.python.org/pep-0646/), so that `Annotated[T, Field(...)]`, `Annotated[T, Unpack[Field(...)]]` and `Annotated[T, *Field(...)]` are all treated consistently.
Libraries consuming annotated-types should also ignore any metadata they do not recongize that came from unpacking a `GroupedMetadata`, just like they ignore unrecognized metadata in `Annotated` itself.
Our own `annotated_types.Interval` class is a `GroupedMetadata` which unpacks itself into `Gt`, `Lt`, etc., so this is not an abstract concern. Similarly, `annotated_types.Len` is a `GroupedMetadata` which unpacks itself into `MinLen` (optionally) and `MaxLen`.
### Consuming metadata
We intend to not be prescriptive as to _how_ the metadata and constraints are used, but as an example of how one might parse constraints from types annotations see our [implementation in `test_main.py`](https://github.com/annotated-types/annotated-types/blob/f59cf6d1b5255a0fe359b93896759a180bec30ae/tests/test_main.py#L94-L103).
It is up to the implementer to determine how this metadata is used.
You could use the metadata for runtime type checking, for generating schemas or to generate example data, amongst other use cases.
## Design & History
This package was designed at the PyCon 2022 sprints by the maintainers of Pydantic
and Hypothesis, with the goal of making it as easy as possible for end-users to
provide more informative annotations for use by runtime libraries.
It is deliberately minimal, and following PEP-593 allows considerable downstream
discretion in what (if anything!) they choose to support. Nonetheless, we expect
that staying simple and covering _only_ the most common use-cases will give users
and maintainers the best experience we can. If you'd like more constraints for your
types - follow our lead, by defining them and documenting them downstream!

View file

@ -0,0 +1,10 @@
annotated_types-0.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
annotated_types-0.7.0.dist-info/METADATA,sha256=7ltqxksJJ0wCYFGBNIQCWTlWQGeAH0hRFdnK3CB895E,15046
annotated_types-0.7.0.dist-info/RECORD,,
annotated_types-0.7.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
annotated_types-0.7.0.dist-info/licenses/LICENSE,sha256=_hBJiEsaDZNCkB6I4H8ykl0ksxIdmXK2poBfuYJLCV0,1083
annotated_types/__init__.py,sha256=RynLsRKUEGI0KimXydlD1fZEfEzWwDo0Uon3zOKhG1Q,13819
annotated_types/__pycache__/__init__.cpython-310.pyc,,
annotated_types/__pycache__/test_cases.cpython-310.pyc,,
annotated_types/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
annotated_types/test_cases.py,sha256=zHFX6EpcMbGJ8FzBYDbO56bPwx_DYIVSKbZM-4B3_lg,6421

View file

@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: hatchling 1.24.2
Root-Is-Purelib: true
Tag: py3-none-any

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2022 the contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,432 @@
import math
import sys
import types
from dataclasses import dataclass
from datetime import tzinfo
from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, SupportsFloat, SupportsIndex, TypeVar, Union
if sys.version_info < (3, 8):
from typing_extensions import Protocol, runtime_checkable
else:
from typing import Protocol, runtime_checkable
if sys.version_info < (3, 9):
from typing_extensions import Annotated, Literal
else:
from typing import Annotated, Literal
if sys.version_info < (3, 10):
EllipsisType = type(Ellipsis)
KW_ONLY = {}
SLOTS = {}
else:
from types import EllipsisType
KW_ONLY = {"kw_only": True}
SLOTS = {"slots": True}
__all__ = (
'BaseMetadata',
'GroupedMetadata',
'Gt',
'Ge',
'Lt',
'Le',
'Interval',
'MultipleOf',
'MinLen',
'MaxLen',
'Len',
'Timezone',
'Predicate',
'LowerCase',
'UpperCase',
'IsDigits',
'IsFinite',
'IsNotFinite',
'IsNan',
'IsNotNan',
'IsInfinite',
'IsNotInfinite',
'doc',
'DocInfo',
'__version__',
)
__version__ = '0.7.0'
T = TypeVar('T')
# arguments that start with __ are considered
# positional only
# see https://peps.python.org/pep-0484/#positional-only-arguments
class SupportsGt(Protocol):
def __gt__(self: T, __other: T) -> bool:
...
class SupportsGe(Protocol):
def __ge__(self: T, __other: T) -> bool:
...
class SupportsLt(Protocol):
def __lt__(self: T, __other: T) -> bool:
...
class SupportsLe(Protocol):
def __le__(self: T, __other: T) -> bool:
...
class SupportsMod(Protocol):
def __mod__(self: T, __other: T) -> T:
...
class SupportsDiv(Protocol):
def __div__(self: T, __other: T) -> T:
...
class BaseMetadata:
"""Base class for all metadata.
This exists mainly so that implementers
can do `isinstance(..., BaseMetadata)` while traversing field annotations.
"""
__slots__ = ()
@dataclass(frozen=True, **SLOTS)
class Gt(BaseMetadata):
"""Gt(gt=x) implies that the value must be greater than x.
It can be used with any type that supports the ``>`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
gt: SupportsGt
@dataclass(frozen=True, **SLOTS)
class Ge(BaseMetadata):
"""Ge(ge=x) implies that the value must be greater than or equal to x.
It can be used with any type that supports the ``>=`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
ge: SupportsGe
@dataclass(frozen=True, **SLOTS)
class Lt(BaseMetadata):
"""Lt(lt=x) implies that the value must be less than x.
It can be used with any type that supports the ``<`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
lt: SupportsLt
@dataclass(frozen=True, **SLOTS)
class Le(BaseMetadata):
"""Le(le=x) implies that the value must be less than or equal to x.
It can be used with any type that supports the ``<=`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
le: SupportsLe
@runtime_checkable
class GroupedMetadata(Protocol):
"""A grouping of multiple objects, like typing.Unpack.
`GroupedMetadata` on its own is not metadata and has no meaning.
All of the constraints and metadata should be fully expressable
in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`.
Concrete implementations should override `GroupedMetadata.__iter__()`
to add their own metadata.
For example:
>>> @dataclass
>>> class Field(GroupedMetadata):
>>> gt: float | None = None
>>> description: str | None = None
...
>>> def __iter__(self) -> Iterable[object]:
>>> if self.gt is not None:
>>> yield Gt(self.gt)
>>> if self.description is not None:
>>> yield Description(self.gt)
Also see the implementation of `Interval` below for an example.
Parsers should recognize this and unpack it so that it can be used
both with and without unpacking:
- `Annotated[int, Field(...)]` (parser must unpack Field)
- `Annotated[int, *Field(...)]` (PEP-646)
""" # noqa: trailing-whitespace
@property
def __is_annotated_types_grouped_metadata__(self) -> Literal[True]:
return True
def __iter__(self) -> Iterator[object]:
...
if not TYPE_CHECKING:
__slots__ = () # allow subclasses to use slots
def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
# Basic ABC like functionality without the complexity of an ABC
super().__init_subclass__(*args, **kwargs)
if cls.__iter__ is GroupedMetadata.__iter__:
raise TypeError("Can't subclass GroupedMetadata without implementing __iter__")
def __iter__(self) -> Iterator[object]: # noqa: F811
raise NotImplementedError # more helpful than "None has no attribute..." type errors
@dataclass(frozen=True, **KW_ONLY, **SLOTS)
class Interval(GroupedMetadata):
"""Interval can express inclusive or exclusive bounds with a single object.
It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which
are interpreted the same way as the single-bound constraints.
"""
gt: Union[SupportsGt, None] = None
ge: Union[SupportsGe, None] = None
lt: Union[SupportsLt, None] = None
le: Union[SupportsLe, None] = None
def __iter__(self) -> Iterator[BaseMetadata]:
"""Unpack an Interval into zero or more single-bounds."""
if self.gt is not None:
yield Gt(self.gt)
if self.ge is not None:
yield Ge(self.ge)
if self.lt is not None:
yield Lt(self.lt)
if self.le is not None:
yield Le(self.le)
@dataclass(frozen=True, **SLOTS)
class MultipleOf(BaseMetadata):
"""MultipleOf(multiple_of=x) might be interpreted in two ways:
1. Python semantics, implying ``value % multiple_of == 0``, or
2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of``
We encourage users to be aware of these two common interpretations,
and libraries to carefully document which they implement.
"""
multiple_of: Union[SupportsDiv, SupportsMod]
@dataclass(frozen=True, **SLOTS)
class MinLen(BaseMetadata):
"""
MinLen() implies minimum inclusive length,
e.g. ``len(value) >= min_length``.
"""
min_length: Annotated[int, Ge(0)]
@dataclass(frozen=True, **SLOTS)
class MaxLen(BaseMetadata):
"""
MaxLen() implies maximum inclusive length,
e.g. ``len(value) <= max_length``.
"""
max_length: Annotated[int, Ge(0)]
@dataclass(frozen=True, **SLOTS)
class Len(GroupedMetadata):
"""
Len() implies that ``min_length <= len(value) <= max_length``.
Upper bound may be omitted or ``None`` to indicate no upper length bound.
"""
min_length: Annotated[int, Ge(0)] = 0
max_length: Optional[Annotated[int, Ge(0)]] = None
def __iter__(self) -> Iterator[BaseMetadata]:
"""Unpack a Len into zone or more single-bounds."""
if self.min_length > 0:
yield MinLen(self.min_length)
if self.max_length is not None:
yield MaxLen(self.max_length)
@dataclass(frozen=True, **SLOTS)
class Timezone(BaseMetadata):
"""Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive).
``Annotated[datetime, Timezone(None)]`` must be a naive datetime.
``Timezone[...]`` (the ellipsis literal) expresses that the datetime must be
tz-aware but any timezone is allowed.
You may also pass a specific timezone string or tzinfo object such as
``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that
you only allow a specific timezone, though we note that this is often
a symptom of poor design.
"""
tz: Union[str, tzinfo, EllipsisType, None]
@dataclass(frozen=True, **SLOTS)
class Unit(BaseMetadata):
"""Indicates that the value is a physical quantity with the specified unit.
It is intended for usage with numeric types, where the value represents the
magnitude of the quantity. For example, ``distance: Annotated[float, Unit('m')]``
or ``speed: Annotated[float, Unit('m/s')]``.
Interpretation of the unit string is left to the discretion of the consumer.
It is suggested to follow conventions established by python libraries that work
with physical quantities, such as
- ``pint`` : <https://pint.readthedocs.io/en/stable/>
- ``astropy.units``: <https://docs.astropy.org/en/stable/units/>
For indicating a quantity with a certain dimensionality but without a specific unit
it is recommended to use square brackets, e.g. `Annotated[float, Unit('[time]')]`.
Note, however, ``annotated_types`` itself makes no use of the unit string.
"""
unit: str
@dataclass(frozen=True, **SLOTS)
class Predicate(BaseMetadata):
"""``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values.
Users should prefer statically inspectable metadata, but if you need the full
power and flexibility of arbitrary runtime predicates... here it is.
We provide a few predefined predicates for common string constraints:
``IsLower = Predicate(str.islower)``, ``IsUpper = Predicate(str.isupper)``, and
``IsDigits = Predicate(str.isdigit)``. Users are encouraged to use methods which
can be given special handling, and avoid indirection like ``lambda s: s.lower()``.
Some libraries might have special logic to handle certain predicates, e.g. by
checking for `str.isdigit` and using its presence to both call custom logic to
enforce digit-only strings, and customise some generated external schema.
We do not specify what behaviour should be expected for predicates that raise
an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
skip invalid constraints, or statically raise an error; or it might try calling it
and then propagate or discard the resulting exception.
"""
func: Callable[[Any], bool]
def __repr__(self) -> str:
if getattr(self.func, "__name__", "<lambda>") == "<lambda>":
return f"{self.__class__.__name__}({self.func!r})"
if isinstance(self.func, (types.MethodType, types.BuiltinMethodType)) and (
namespace := getattr(self.func.__self__, "__name__", None)
):
return f"{self.__class__.__name__}({namespace}.{self.func.__name__})"
if isinstance(self.func, type(str.isascii)): # method descriptor
return f"{self.__class__.__name__}({self.func.__qualname__})"
return f"{self.__class__.__name__}({self.func.__name__})"
@dataclass
class Not:
func: Callable[[Any], bool]
def __call__(self, __v: Any) -> bool:
return not self.func(__v)
_StrType = TypeVar("_StrType", bound=str)
LowerCase = Annotated[_StrType, Predicate(str.islower)]
"""
Return True if the string is a lowercase string, False otherwise.
A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string.
""" # noqa: E501
UpperCase = Annotated[_StrType, Predicate(str.isupper)]
"""
Return True if the string is an uppercase string, False otherwise.
A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string.
""" # noqa: E501
IsDigit = Annotated[_StrType, Predicate(str.isdigit)]
IsDigits = IsDigit # type: ignore # plural for backwards compatibility, see #63
"""
Return True if the string is a digit string, False otherwise.
A string is a digit string if all characters in the string are digits and there is at least one character in the string.
""" # noqa: E501
IsAscii = Annotated[_StrType, Predicate(str.isascii)]
"""
Return True if all characters in the string are ASCII, False otherwise.
ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too.
"""
_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex])
IsFinite = Annotated[_NumericType, Predicate(math.isfinite)]
"""Return True if x is neither an infinity nor a NaN, and False otherwise."""
IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))]
"""Return True if x is one of infinity or NaN, and False otherwise"""
IsNan = Annotated[_NumericType, Predicate(math.isnan)]
"""Return True if x is a NaN (not a number), and False otherwise."""
IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))]
"""Return True if x is anything but NaN (not a number), and False otherwise."""
IsInfinite = Annotated[_NumericType, Predicate(math.isinf)]
"""Return True if x is a positive or negative infinity, and False otherwise."""
IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))]
"""Return True if x is neither a positive or negative infinity, and False otherwise."""
try:
from typing_extensions import DocInfo, doc # type: ignore [attr-defined]
except ImportError:
@dataclass(frozen=True, **SLOTS)
class DocInfo: # type: ignore [no-redef]
""" "
The return value of doc(), mainly to be used by tools that want to extract the
Annotated documentation at runtime.
"""
documentation: str
"""The documentation string passed to doc()."""
def doc(
documentation: str,
) -> DocInfo:
"""
Add documentation to a type annotation inside of Annotated.
For example:
>>> def hi(name: Annotated[int, doc("The name of the user")]) -> None: ...
"""
return DocInfo(documentation)

View file

@ -0,0 +1,151 @@
import math
import sys
from datetime import date, datetime, timedelta, timezone
from decimal import Decimal
from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Set, Tuple
if sys.version_info < (3, 9):
from typing_extensions import Annotated
else:
from typing import Annotated
import annotated_types as at
class Case(NamedTuple):
"""
A test case for `annotated_types`.
"""
annotation: Any
valid_cases: Iterable[Any]
invalid_cases: Iterable[Any]
def cases() -> Iterable[Case]:
# Gt, Ge, Lt, Le
yield Case(Annotated[int, at.Gt(4)], (5, 6, 1000), (4, 0, -1))
yield Case(Annotated[float, at.Gt(0.5)], (0.6, 0.7, 0.8, 0.9), (0.5, 0.0, -0.1))
yield Case(
Annotated[datetime, at.Gt(datetime(2000, 1, 1))],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
)
yield Case(
Annotated[datetime, at.Gt(date(2000, 1, 1))],
[date(2000, 1, 2), date(2000, 1, 3)],
[date(2000, 1, 1), date(1999, 12, 31)],
)
yield Case(
Annotated[datetime, at.Gt(Decimal('1.123'))],
[Decimal('1.1231'), Decimal('123')],
[Decimal('1.123'), Decimal('0')],
)
yield Case(Annotated[int, at.Ge(4)], (4, 5, 6, 1000, 4), (0, -1))
yield Case(Annotated[float, at.Ge(0.5)], (0.5, 0.6, 0.7, 0.8, 0.9), (0.4, 0.0, -0.1))
yield Case(
Annotated[datetime, at.Ge(datetime(2000, 1, 1))],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
[datetime(1998, 1, 1), datetime(1999, 12, 31)],
)
yield Case(Annotated[int, at.Lt(4)], (0, -1), (4, 5, 6, 1000, 4))
yield Case(Annotated[float, at.Lt(0.5)], (0.4, 0.0, -0.1), (0.5, 0.6, 0.7, 0.8, 0.9))
yield Case(
Annotated[datetime, at.Lt(datetime(2000, 1, 1))],
[datetime(1999, 12, 31), datetime(1999, 12, 31)],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
)
yield Case(Annotated[int, at.Le(4)], (4, 0, -1), (5, 6, 1000))
yield Case(Annotated[float, at.Le(0.5)], (0.5, 0.0, -0.1), (0.6, 0.7, 0.8, 0.9))
yield Case(
Annotated[datetime, at.Le(datetime(2000, 1, 1))],
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
)
# Interval
yield Case(Annotated[int, at.Interval(gt=4)], (5, 6, 1000), (4, 0, -1))
yield Case(Annotated[int, at.Interval(gt=4, lt=10)], (5, 6), (4, 10, 1000, 0, -1))
yield Case(Annotated[float, at.Interval(ge=0.5, le=1)], (0.5, 0.9, 1), (0.49, 1.1))
yield Case(
Annotated[datetime, at.Interval(gt=datetime(2000, 1, 1), le=datetime(2000, 1, 3))],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
[datetime(2000, 1, 1), datetime(2000, 1, 4)],
)
yield Case(Annotated[int, at.MultipleOf(multiple_of=3)], (0, 3, 9), (1, 2, 4))
yield Case(Annotated[float, at.MultipleOf(multiple_of=0.5)], (0, 0.5, 1, 1.5), (0.4, 1.1))
# lengths
yield Case(Annotated[str, at.MinLen(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
yield Case(Annotated[str, at.Len(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
yield Case(Annotated[List[int], at.MinLen(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
yield Case(Annotated[List[int], at.Len(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
yield Case(Annotated[str, at.MaxLen(4)], ('', '1234'), ('12345', 'x' * 10))
yield Case(Annotated[str, at.Len(0, 4)], ('', '1234'), ('12345', 'x' * 10))
yield Case(Annotated[List[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
yield Case(Annotated[List[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
yield Case(Annotated[str, at.Len(3, 5)], ('123', '12345'), ('', '1', '12', '123456', 'x' * 10))
yield Case(Annotated[str, at.Len(3, 3)], ('123',), ('12', '1234'))
yield Case(Annotated[Dict[int, int], at.Len(2, 3)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}])
yield Case(Annotated[Set[int], at.Len(2, 3)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4}))
yield Case(Annotated[Tuple[int, ...], at.Len(2, 3)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4)))
# Timezone
yield Case(
Annotated[datetime, at.Timezone(None)], [datetime(2000, 1, 1)], [datetime(2000, 1, 1, tzinfo=timezone.utc)]
)
yield Case(
Annotated[datetime, at.Timezone(...)], [datetime(2000, 1, 1, tzinfo=timezone.utc)], [datetime(2000, 1, 1)]
)
yield Case(
Annotated[datetime, at.Timezone(timezone.utc)],
[datetime(2000, 1, 1, tzinfo=timezone.utc)],
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
)
yield Case(
Annotated[datetime, at.Timezone('Europe/London')],
[datetime(2000, 1, 1, tzinfo=timezone(timedelta(0), name='Europe/London'))],
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
)
# Quantity
yield Case(Annotated[float, at.Unit(unit='m')], (5, 4.2), ('5m', '4.2m'))
# predicate types
yield Case(at.LowerCase[str], ['abc', 'foobar'], ['', 'A', 'Boom'])
yield Case(at.UpperCase[str], ['ABC', 'DEFO'], ['', 'a', 'abc', 'AbC'])
yield Case(at.IsDigit[str], ['123'], ['', 'ab', 'a1b2'])
yield Case(at.IsAscii[str], ['123', 'foo bar'], ['£100', '😊', 'whatever 👀'])
yield Case(Annotated[int, at.Predicate(lambda x: x % 2 == 0)], [0, 2, 4], [1, 3, 5])
yield Case(at.IsFinite[float], [1.23], [math.nan, math.inf, -math.inf])
yield Case(at.IsNotFinite[float], [math.nan, math.inf], [1.23])
yield Case(at.IsNan[float], [math.nan], [1.23, math.inf])
yield Case(at.IsNotNan[float], [1.23, math.inf], [math.nan])
yield Case(at.IsInfinite[float], [math.inf], [math.nan, 1.23])
yield Case(at.IsNotInfinite[float], [math.nan, 1.23], [math.inf])
# check stacked predicates
yield Case(at.IsInfinite[Annotated[float, at.Predicate(lambda x: x > 0)]], [math.inf], [-math.inf, 1.23, math.nan])
# doc
yield Case(Annotated[int, at.doc("A number")], [1, 2], [])
# custom GroupedMetadata
class MyCustomGroupedMetadata(at.GroupedMetadata):
def __iter__(self) -> Iterator[at.Predicate]:
yield at.Predicate(lambda x: float(x).is_integer())
yield Case(Annotated[float, MyCustomGroupedMetadata()], [0, 2.0], [0.01, 1.5])

View file

@ -0,0 +1,354 @@
Metadata-Version: 2.4
Name: argcomplete
Version: 3.6.3
Summary: Bash tab completion for argparse
Project-URL: Homepage, https://github.com/kislyuk/argcomplete
Project-URL: Documentation, https://kislyuk.github.io/argcomplete
Project-URL: Source Code, https://github.com/kislyuk/argcomplete
Project-URL: Issue Tracker, https://github.com/kislyuk/argcomplete/issues
Project-URL: Change Log, https://github.com/kislyuk/argcomplete/blob/develop/Changes.rst
Author: Andrey Kislyuk
Author-email: kislyuk@gmail.com
Maintainer: Andrey Kislyuk
Maintainer-email: kislyuk@gmail.com
License: Apache Software License
License-File: LICENSE.rst
License-File: NOTICE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Shells
Classifier: Topic :: Terminals
Requires-Python: >=3.8
Provides-Extra: test
Requires-Dist: coverage; extra == 'test'
Requires-Dist: mypy; extra == 'test'
Requires-Dist: pexpect; extra == 'test'
Requires-Dist: ruff; extra == 'test'
Requires-Dist: wheel; extra == 'test'
Description-Content-Type: text/x-rst
argcomplete - Bash/zsh tab completion for argparse
==================================================
*Tab complete all the things!*
Argcomplete provides easy, extensible command line tab completion of arguments for your Python application.
It makes two assumptions:
* You're using bash or zsh as your shell (limited support exists for other shells - see below)
* You're using `argparse <http://docs.python.org/3/library/argparse.html>`_ to manage your command line arguments/options
Argcomplete is particularly useful if your program has lots of options or subparsers, and if your program can
dynamically suggest completions for your argument/option values (for example, if the user is browsing resources over
the network).
Installation
------------
::
pip install argcomplete
activate-global-python-argcomplete
See `Activating global completion`_ below for details about the second step.
Refresh your shell environment (start a new shell).
Synopsis
--------
Add the ``PYTHON_ARGCOMPLETE_OK`` marker and a call to ``argcomplete.autocomplete()`` to your Python application as
follows:
.. code-block:: python
#!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK
import argcomplete, argparse
parser = argparse.ArgumentParser()
...
argcomplete.autocomplete(parser)
args = parser.parse_args()
...
If using ``pyproject.toml`` ``[project.scripts]`` entry points, the ``PYTHON_ARGCOMPLETE_OK`` marker should appear
at the beginning of the file that contains the entry point.
Register your Python application with your shell's completion framework by running ``register-python-argcomplete``::
eval "$(register-python-argcomplete my-python-app)"
Quotes are significant; the registration will fail without them. See `Global completion`_ below for a way to enable
argcomplete generally without registering each application individually.
argcomplete.autocomplete(*parser*)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This method is the entry point to the module. It must be called **after** ArgumentParser construction is complete, but
**before** the ``ArgumentParser.parse_args()`` method is called. The method looks for an environment variable that the
completion hook shellcode sets, and if it's there, collects completions, prints them to the output stream (fd 8 by
default), and exits. Otherwise, it returns to the caller immediately.
.. admonition:: Side effects
Argcomplete gets completions by running your program. It intercepts the execution flow at the moment
``argcomplete.autocomplete()`` is called. After sending completions, it exits using ``exit_method`` (``os._exit``
by default). This means if your program has any side effects that happen before ``argcomplete`` is called, those
side effects will happen every time the user presses ``<TAB>`` (although anything your program prints to stdout or
stderr will be suppressed). For this reason it's best to construct the argument parser and call
``argcomplete.autocomplete()`` as early as possible in your execution flow.
.. admonition:: Performance
If the program takes a long time to get to the point where ``argcomplete.autocomplete()`` is called, the tab completion
process will feel sluggish, and the user may lose confidence in it. So it's also important to minimize the startup time
of the program up to that point (for example, by deferring initialization or importing of large modules until after
parsing options).
Specifying completers
---------------------
You can specify custom completion functions for your options and arguments. Two styles are supported: callable and
readline-style. Callable completers are simpler. They are called with the following keyword arguments:
* ``prefix``: The prefix text of the last word before the cursor on the command line.
For dynamic completers, this can be used to reduce the work required to generate possible completions.
* ``action``: The ``argparse.Action`` instance that this completer was called for.
* ``parser``: The ``argparse.ArgumentParser`` instance that the action was taken by.
* ``parsed_args``: The result of argument parsing so far (the ``argparse.Namespace`` args object normally returned by
``ArgumentParser.parse_args()``).
Completers can return their completions as an iterable of strings or a mapping (dict) of strings to their
descriptions (zsh will display the descriptions as context help alongside completions). An example completer for names
of environment variables might look like this:
.. code-block:: python
def EnvironCompleter(**kwargs):
return os.environ
To specify a completer for an argument or option, set the ``completer`` attribute of its associated action. An easy
way to do this at definition time is:
.. code-block:: python
from argcomplete.completers import EnvironCompleter
parser = argparse.ArgumentParser()
parser.add_argument("--env-var1").completer = EnvironCompleter
parser.add_argument("--env-var2").completer = EnvironCompleter
argcomplete.autocomplete(parser)
If you specify the ``choices`` keyword for an argparse option or argument (and don't specify a completer), it will be
used for completions.
A completer that is initialized with a set of all possible choices of values for its action might look like this:
.. code-block:: python
class ChoicesCompleter(object):
def __init__(self, choices):
self.choices = choices
def __call__(self, **kwargs):
return self.choices
The following two ways to specify a static set of choices are equivalent for completion purposes:
.. code-block:: python
from argcomplete.completers import ChoicesCompleter
parser.add_argument("--protocol", choices=('http', 'https', 'ssh', 'rsync', 'wss'))
parser.add_argument("--proto").completer=ChoicesCompleter(('http', 'https', 'ssh', 'rsync', 'wss'))
Note that if you use the ``choices=<completions>`` option, argparse will show
all these choices in the ``--help`` output by default. To prevent this, set
``metavar`` (like ``parser.add_argument("--protocol", metavar="PROTOCOL",
choices=('http', 'https', 'ssh', 'rsync', 'wss'))``).
The following `script <https://raw.github.com/kislyuk/argcomplete/master/docs/examples/describe_github_user.py>`_ uses
``parsed_args`` and `Requests <http://python-requests.org/>`_ to query GitHub for publicly known members of an
organization and complete their names, then prints the member description:
.. code-block:: python
#!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK
import argcomplete, argparse, requests, pprint
def github_org_members(prefix, parsed_args, **kwargs):
resource = "https://api.github.com/orgs/{org}/members".format(org=parsed_args.organization)
return (member['login'] for member in requests.get(resource).json() if member['login'].startswith(prefix))
parser = argparse.ArgumentParser()
parser.add_argument("--organization", help="GitHub organization")
parser.add_argument("--member", help="GitHub member").completer = github_org_members
argcomplete.autocomplete(parser)
args = parser.parse_args()
pprint.pprint(requests.get("https://api.github.com/users/{m}".format(m=args.member)).json())
`Try it <https://raw.github.com/kislyuk/argcomplete/master/docs/examples/describe_github_user.py>`_ like this::
./describe_github_user.py --organization heroku --member <TAB>
If you have a useful completer to add to the `completer library
<https://github.com/kislyuk/argcomplete/blob/master/argcomplete/completers.py>`_, send a pull request!
Readline-style completers
~~~~~~~~~~~~~~~~~~~~~~~~~
The readline_ module defines a completer protocol in rlcompleter_. Readline-style completers are also supported by
argcomplete, so you can use the same completer object both in an interactive readline-powered shell and on the command
line. For example, you can use the readline-style completer provided by IPython_ to get introspective completions like
you would get in the IPython shell:
.. _readline: http://docs.python.org/3/library/readline.html
.. _rlcompleter: http://docs.python.org/3/library/rlcompleter.html#completer-objects
.. _IPython: http://ipython.org/
.. code-block:: python
import IPython
parser.add_argument("--python-name").completer = IPython.core.completer.Completer()
``argcomplete.CompletionFinder.rl_complete`` can also be used to plug in an argparse parser as a readline completer.
Printing warnings in completers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Normal stdout/stderr output is suspended when argcomplete runs. Sometimes, though, when the user presses ``<TAB>``, it's
appropriate to print information about why completions generation failed. To do this, use ``warn``:
.. code-block:: python
from argcomplete import warn
def AwesomeWebServiceCompleter(prefix, **kwargs):
if login_failed:
warn("Please log in to Awesome Web Service to use autocompletion")
return completions
Using a custom completion validator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
By default, argcomplete validates your completions by checking if they start with the prefix given to the completer. You
can override this validation check by supplying the ``validator`` keyword to ``argcomplete.autocomplete()``:
.. code-block:: python
def my_validator(completion_candidate, current_input):
"""Complete non-prefix substring matches."""
return current_input in completion_candidate
argcomplete.autocomplete(parser, validator=my_validator)
Global completion
-----------------
In global completion mode, you don't have to register each argcomplete-capable executable separately. Instead, the shell
will look for the string **PYTHON_ARGCOMPLETE_OK** in the first 1024 bytes of any executable that it's running
completion for, and if it's found, follow the rest of the argcomplete protocol as described above.
Additionally, completion is activated for scripts run as ``python <script>`` and ``python -m <module>``. If you're using
multiple Python versions on the same system, the version being used to run the script must have argcomplete installed.
.. admonition:: Bash version compatibility
When using bash, global completion requires bash support for ``complete -D``, which was introduced in bash 4.2. Since
Mac OS ships with an outdated version of Bash (3.2), you can either use zsh or install a newer version of bash using
`Homebrew <http://brew.sh/>`_ (``brew install bash`` - you will also need to add ``/opt/homebrew/bin/bash`` to
``/etc/shells``, and run ``chsh`` to change your shell). You can check the version of the running copy of bash with
``echo $BASH_VERSION``.
.. note:: If you use ``project.scripts`` directives to provide command line entry points to your package,
argcomplete will follow the wrapper scripts to their destination and look for ``PYTHON_ARGCOMPLETE_OK`` in the
first kilobyte of the file containing the destination code.
If you choose not to use global completion, or ship a completion module that depends on argcomplete, you must register
your script explicitly using ``eval "$(register-python-argcomplete my-python-app)"``. Standard completion module
registration rules apply: namely, the script name is passed directly to ``complete``, meaning it is only tab completed
when invoked exactly as it was registered. In the above example, ``my-python-app`` must be on the path, and the user
must be attempting to complete it by that name. The above line alone would **not** allow you to complete
``./my-python-app``, or ``/path/to/my-python-app``.
Activating global completion
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The script ``activate-global-python-argcomplete`` installs the global completion script
`bash_completion.d/_python-argcomplete <https://github.com/kislyuk/argcomplete/blob/master/argcomplete/bash_completion.d/_python-argcomplete>`_
into an appropriate location on your system for both bash and zsh. The specific location depends on your platform and
whether you installed argcomplete system-wide using ``sudo`` or locally (into your user's home directory).
Zsh Support
-----------
Argcomplete supports zsh. On top of plain completions like in bash, zsh allows you to see argparse help strings as
completion descriptions. All shellcode included with argcomplete is compatible with both bash and zsh, so the same
completer commands ``activate-global-python-argcomplete`` and ``eval "$(register-python-argcomplete my-python-app)"``
work for zsh as well.
Python Support
--------------
Argcomplete requires Python 3.9+.
Support for other shells
------------------------
Argcomplete maintainers provide support only for the bash and zsh shells on Linux and MacOS. For resources related to
other shells and platforms, including fish, tcsh, xonsh, powershell, and Windows, please see the
`contrib <https://github.com/kislyuk/argcomplete/tree/develop/contrib>`_ directory.
Common Problems
---------------
If global completion is not completing your script, bash may have registered a default completion function::
$ complete | grep my-python-app
complete -F _minimal my-python-app
You can fix this by restarting your shell, or by running ``complete -r my-python-app``.
Debugging
---------
Set the ``_ARC_DEBUG`` variable in your shell to enable verbose debug output every time argcomplete runs. This will
disrupt the command line composition state of your terminal, but make it possible to see the internal state of the
completer if it encounters problems.
Acknowledgments
---------------
Inspired and informed by the optcomplete_ module by Martin Blais.
.. _optcomplete: http://pypi.python.org/pypi/optcomplete
Links
-----
* `Project home page (GitHub) <https://github.com/kislyuk/argcomplete>`_
* `Documentation <https://kislyuk.github.io/argcomplete/>`_
* `Package distribution (PyPI) <https://pypi.python.org/pypi/argcomplete>`_
* `Change log <https://github.com/kislyuk/argcomplete/blob/master/Changes.rst>`_
Bugs
~~~~
Please report bugs, issues, feature requests, etc. on `GitHub <https://github.com/kislyuk/argcomplete/issues>`_.
License
-------
Copyright 2012-2023, Andrey Kislyuk and argcomplete contributors. Licensed under the terms of the
`Apache License, Version 2.0 <http://www.apache.org/licenses/LICENSE-2.0>`_. Distribution of the LICENSE and NOTICE
files with source copies of this package and derivative works is **REQUIRED** as specified by the Apache License.
.. image:: https://github.com/kislyuk/argcomplete/workflows/Python%20package/badge.svg
:target: https://github.com/kislyuk/argcomplete/actions
.. image:: https://codecov.io/github/kislyuk/argcomplete/coverage.svg?branch=master
:target: https://codecov.io/github/kislyuk/argcomplete?branch=master
.. image:: https://img.shields.io/pypi/v/argcomplete.svg
:target: https://pypi.python.org/pypi/argcomplete
.. image:: https://img.shields.io/pypi/l/argcomplete.svg
:target: https://pypi.python.org/pypi/argcomplete

View file

@ -0,0 +1,44 @@
../../Scripts/activate-global-python-argcomplete.exe,sha256=sKTtEcxvniDHRL7TCczFj2bZGEoUZeP72CVONpB7PUk,106438
../../Scripts/python-argcomplete-check-easy-install-script.exe,sha256=OSI2DrC05tXplss6QmCac3ktWpVA-3hX0SRRM9XftSI,106448
../../Scripts/register-python-argcomplete.exe,sha256=nR87Gcqu9tSQUWELXcNsu9-7HFeZl7p2NMoUV1qs568,106431
argcomplete-3.6.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
argcomplete-3.6.3.dist-info/METADATA,sha256=pgeGA7r-pv0MtwGW3gjQCI7P7959Re96W9ppfBxkCMc,16891
argcomplete-3.6.3.dist-info/RECORD,,
argcomplete-3.6.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
argcomplete-3.6.3.dist-info/entry_points.txt,sha256=b5M9f6w7ZYVDGLGhZOghcXTwAlVJJIDnnuaJsafvaIo,315
argcomplete-3.6.3.dist-info/licenses/LICENSE.rst,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
argcomplete-3.6.3.dist-info/licenses/NOTICE,sha256=LIicch7Irm12ZGgK-u-7THYgl29DS1elBuzZLwZJtqA,387
argcomplete/__init__.py,sha256=yF6xrvfK0WpO8rlqQlilfScgXTJH1RSXcAMi5s4iZ6Q,693
argcomplete/__pycache__/__init__.cpython-310.pyc,,
argcomplete/__pycache__/_check_console_script.cpython-310.pyc,,
argcomplete/__pycache__/_check_module.cpython-310.pyc,,
argcomplete/__pycache__/completers.cpython-310.pyc,,
argcomplete/__pycache__/exceptions.cpython-310.pyc,,
argcomplete/__pycache__/finders.cpython-310.pyc,,
argcomplete/__pycache__/io.cpython-310.pyc,,
argcomplete/__pycache__/lexers.cpython-310.pyc,,
argcomplete/__pycache__/shell_integration.cpython-310.pyc,,
argcomplete/_check_console_script.py,sha256=pL8QMk-0RB2_dakNTAy5aGA-2sB2fY2E7Ps3LSLi2sQ,2413
argcomplete/_check_module.py,sha256=vBHBICA_sRcduWArcrupNimkum-9DApa7zh8AauFSUo,2178
argcomplete/bash_completion.d/_python-argcomplete,sha256=Bq8DgQlSyKVYrnp1aqG0YVEyQbKk8vB9DIcazd-dJqU,10047
argcomplete/completers.py,sha256=_AroOMLJirJe786_7pf4hA83l6IPIlzaES-uKJcz150,4657
argcomplete/exceptions.py,sha256=tojpetbrtcmabLRVC_YqiFGv0x-QzHPOIA2fEG6l9aA,112
argcomplete/finders.py,sha256=yxYDCePC9bIfe64wcw1YsmAvrE3spKQ0iO1MHDnFUuY,28664
argcomplete/io.py,sha256=PCepOaNi714v6aEzQ7AAltWDYo-aP5Xmbm6-fJ4ta5U,866
argcomplete/lexers.py,sha256=se52bPTUK97m8oGDqP0mrwxQEUYMnOxvGQ2FdEo8v1U,2131
argcomplete/packages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
argcomplete/packages/__pycache__/__init__.cpython-310.pyc,,
argcomplete/packages/__pycache__/_argparse.cpython-310.pyc,,
argcomplete/packages/__pycache__/_shlex.cpython-310.pyc,,
argcomplete/packages/_argparse.py,sha256=BdSoZUpmc-LlT8pAjLmQE6eh98CdKU78tJDRy5HmWEE,16151
argcomplete/packages/_shlex.py,sha256=hQ9e0n_oKsbKzuIQk0kcABK7cZO1bRGg-tpFGCTpO_4,12963
argcomplete/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
argcomplete/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
argcomplete/scripts/__pycache__/__init__.cpython-310.pyc,,
argcomplete/scripts/__pycache__/activate_global_python_argcomplete.cpython-310.pyc,,
argcomplete/scripts/__pycache__/python_argcomplete_check_easy_install_script.cpython-310.pyc,,
argcomplete/scripts/__pycache__/register_python_argcomplete.cpython-310.pyc,,
argcomplete/scripts/activate_global_python_argcomplete.py,sha256=DqNPgvT4XVz2AjcfRePio8kSHJ3_SU0jIE1-rmqk-PA,6454
argcomplete/scripts/python_argcomplete_check_easy_install_script.py,sha256=4FMHlpfFK2mdez-aH_sEKiu6kcUkbdjdD2AtBr6cpuw,3478
argcomplete/scripts/register_python_argcomplete.py,sha256=oDspOBZPfXhck5z9KPMH0Wls9VVe2Kw7n9ARS5NczfQ,2242
argcomplete/shell_integration.py,sha256=rlHtjyjobQSIE6AD-_KFyBFzCCRpApgADfaPOR9dumY,7915

View file

@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: hatchling 1.27.0
Root-Is-Purelib: true
Tag: py3-none-any

View file

@ -0,0 +1,4 @@
[console_scripts]
activate-global-python-argcomplete = argcomplete.scripts.activate_global_python_argcomplete:main
python-argcomplete-check-easy-install-script = argcomplete.scripts.python_argcomplete_check_easy_install_script:main
register-python-argcomplete = argcomplete.scripts.register_python_argcomplete:main

View file

@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View file

@ -0,0 +1,4 @@
argcomplete is a free open source library that integrates Python applications with Bash and Zsh shell completion.
The argcomplete project is staffed by volunteers. If you are using this library in a for-profit project, please
contribute to argcomplete development and maintenance using the "Sponsor" button on the argcomplete GitHub project page,
https://github.com/kislyuk/argcomplete.

View file

@ -0,0 +1,13 @@
# Copyright 2012-2023, Andrey Kislyuk and argcomplete contributors.
# Licensed under the Apache License. See https://github.com/kislyuk/argcomplete for more info.
from . import completers
from .completers import ChoicesCompleter, DirectoriesCompleter, EnvironCompleter, FilesCompleter, SuppressCompleter
from .exceptions import ArgcompleteException
from .finders import CompletionFinder, ExclusiveCompletionFinder, safe_actions
from .io import debug, mute_stderr, warn
from .lexers import split_line
from .shell_integration import shellcode
autocomplete = CompletionFinder()
autocomplete.__doc__ = """ Use this to access argcomplete. See :meth:`argcomplete.CompletionFinder.__call__()`. """

View file

@ -0,0 +1,65 @@
"""
Utility for locating the module (or package's __init__.py)
associated with a given console_script name
and verifying it contains the PYTHON_ARGCOMPLETE_OK marker.
Such scripts are automatically generated and cannot contain
the marker themselves, so we defer to the containing module or package.
For more information on setuptools console_scripts, see
https://setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation
Intended to be invoked by argcomplete's global completion function.
"""
import os
import sys
from importlib.metadata import EntryPoint
from importlib.metadata import entry_points as importlib_entry_points
from typing import Iterable
from ._check_module import ArgcompleteMarkerNotFound, find
def main():
# Argument is the full path to the console script.
script_path = sys.argv[1]
# Find the module and function names that correspond to this
# assuming it is actually a console script.
name = os.path.basename(script_path)
entry_points: Iterable[EntryPoint] = importlib_entry_points() # type:ignore
# Python 3.12+ returns a tuple of entry point objects
# whereas <=3.11 returns a SelectableGroups object
if sys.version_info < (3, 12):
entry_points = entry_points["console_scripts"] # type:ignore
entry_points = [ep for ep in entry_points if ep.name == name and ep.group == "console_scripts"] # type:ignore
if not entry_points:
raise ArgcompleteMarkerNotFound("no entry point found matching script")
entry_point = entry_points[0]
module_name, function_name = entry_point.value.split(":", 1)
# Check this looks like the script we really expected.
with open(script_path) as f:
script = f.read()
if "from {} import {}".format(module_name, function_name) not in script:
raise ArgcompleteMarkerNotFound("does not appear to be a console script")
if "sys.exit({}())".format(function_name) not in script:
raise ArgcompleteMarkerNotFound("does not appear to be a console script")
# Look for the argcomplete marker in the script it imports.
with open(find(module_name, return_package=True)) as f:
head = f.read(1024)
if "PYTHON_ARGCOMPLETE_OK" not in head:
raise ArgcompleteMarkerNotFound("marker not found")
if __name__ == "__main__":
try:
main()
except ArgcompleteMarkerNotFound as e:
sys.exit(str(e))

View file

@ -0,0 +1,72 @@
"""
Utility for locating a module (or package's __main__.py) with a given name
and verifying it contains the PYTHON_ARGCOMPLETE_OK marker.
The module name should be specified in a form usable with `python -m`.
Intended to be invoked by argcomplete's global completion function.
"""
import os
import sys
import tokenize
from importlib.util import find_spec
class ArgcompleteMarkerNotFound(RuntimeError):
pass
def find(name, return_package=False):
names = name.split(".")
# Look for the first importlib ModuleSpec that has `origin` set, indicating it's not a namespace package.
for package_name_boundary in range(len(names)):
spec = find_spec(".".join(names[: package_name_boundary + 1]))
if spec is not None and spec.origin is not None:
break
if spec is None:
raise ArgcompleteMarkerNotFound('no module named "{}"'.format(names[0]))
if not spec.has_location:
raise ArgcompleteMarkerNotFound("cannot locate file")
if spec.submodule_search_locations is None:
if len(names) != 1:
raise ArgcompleteMarkerNotFound("{} is not a package".format(names[0]))
return spec.origin
if len(spec.submodule_search_locations) != 1:
raise ArgcompleteMarkerNotFound("expecting one search location")
path = os.path.join(spec.submodule_search_locations[0], *names[package_name_boundary + 1 :])
if os.path.isdir(path):
filename = "__main__.py"
if return_package:
filename = "__init__.py"
return os.path.join(path, filename)
else:
return path + ".py"
def main():
try:
name = sys.argv[1]
except IndexError:
raise ArgcompleteMarkerNotFound("missing argument on the command line")
filename = find(name)
try:
fp = tokenize.open(filename)
except OSError:
raise ArgcompleteMarkerNotFound("cannot open file")
with fp:
head = fp.read(1024)
if "PYTHON_ARGCOMPLETE_OK" not in head:
raise ArgcompleteMarkerNotFound("marker not found")
if __name__ == "__main__":
try:
main()
except ArgcompleteMarkerNotFound as e:
sys.exit(str(e))

View file

@ -0,0 +1,264 @@
#compdef -default-
# argcomplete global completion loader for zsh and bash
# Copyright 2012-2023, Andrey Kislyuk and argcomplete contributors.
# Licensed under the Apache License. See https://github.com/kislyuk/argcomplete for more info.
# Note: both the leading underscore in the name of this file and the first line (compdef) are required by zsh
# In zsh, this file is autoloaded and used as the default completer (_default).
# There are many other special contexts we don't want to override
# (as would be the case with `#compdef -P *`).
# https://zsh.sourceforge.io/Doc/Release/Completion-System.html
# Copy of __expand_tilde_by_ref from bash-completion
# ZSH implementation added
__python_argcomplete_expand_tilde_by_ref () {
if [ -n "${ZSH_VERSION-}" ]; then
if [ "${(P)1[1]}" = "~" ]; then
eval $1="${(P)1/#\~/$HOME}";
fi
else
if [ "${!1:0:1}" = "~" ]; then
if [ "${!1}" != "${!1//\/}" ]; then
eval $1="${!1/%\/*}"/'${!1#*/}';
else
eval $1="${!1}";
fi;
fi
fi
}
# Run something, muting output or redirecting it to the debug stream
# depending on the value of _ARC_DEBUG.
# If ARGCOMPLETE_USE_TEMPFILES is set, use tempfiles for IPC.
__python_argcomplete_run() {
if [[ -z "${ARGCOMPLETE_USE_TEMPFILES-}" ]]; then
__python_argcomplete_run_inner "$@"
return
fi
local tmpfile="$(mktemp)"
_ARGCOMPLETE_STDOUT_FILENAME="$tmpfile" __python_argcomplete_run_inner "$@"
local code=$?
cat "$tmpfile"
rm "$tmpfile"
return $code
}
__python_argcomplete_run_inner() {
if [[ -z "${_ARC_DEBUG-}" ]]; then
"$@" 8>&1 9>&2 1>/dev/null 2>&1 </dev/null
else
"$@" 8>&1 9>&2 1>&9 2>&1 </dev/null
fi
}
__python_argcomplete_upshift_bash_rematch() {
if [[ -z "${ZSH_VERSION-}" ]]; then
_BASH_REMATCH=( "" "${BASH_REMATCH[@]}" )
else
_BASH_REMATCH=( "${BASH_REMATCH[@]}" )
fi
}
# This function scans the beginning of an executable file provided as the first
# argument ($1) for certain indicators, specified by the second argument ($2),
# or the "target". There are three possible targets: "interpreter",
# "magic_string", and "easy_install". If the target is "interpreter", the
# function matches the interpreter line, alongside any optional interpreter
# arguments. If the target is "magic_string", a match is attempted for the
# "PYTHON_ARGCOMPLETE_OK" magic string, indicating that the file should be run
# to get completions. If the target is "easy_install", the function matches either
# "PBR Generated" or any of the "EASY-INSTALL" scripts (either SCRIPT,
# ENTRY-SCRIPT, or DEV-SCRIPT). In all cases, only the first kilobyte of
# the file is searched. The regex matches are returned in BASH_REMATCH,
# indexed starting at 1, regardless of the shell in use.
__python_argcomplete_scan_head() {
local file="$1"
local target="$2"
local REPLY
if [[ -n "${ZSH_VERSION-}" ]]; then
read -r -k 1024 -u 0 < "$file";
else
read -r -N 1024 < "$file"
fi
# Since ZSH does not support a -n option, we
# trim all characters after the first line in both shells
if [[ "$target" = "interpreter" ]]; then
read -r <<< "$REPLY"
fi
local regex
case "$target" in
magic_string) regex='PYTHON_ARGCOMPLETE_OK' ;;
easy_install) regex="(PBR Generated)|(EASY-INSTALL-(SCRIPT|ENTRY-SCRIPT|DEV-SCRIPT))" ;;
asdf) regex="asdf exec " ;;
interpreter) regex='^#!(.*)$' ;;
esac
local ret=""
if [[ "$REPLY" =~ $regex ]]; then
ret=1
fi
__python_argcomplete_upshift_bash_rematch
[[ -n $ret ]]
}
__python_argcomplete_scan_head_noerr() {
__python_argcomplete_scan_head "$@" 2>/dev/null
}
__python_argcomplete_which() {
if [[ -n "${ZSH_VERSION-}" ]]; then
whence -p "$@"
else
type -P "$@"
fi
}
_python_argcomplete_global() {
if [[ -n "${ZSH_VERSION-}" ]]; then
# Store result of a regex match in the
# BASH_REMATCH variable rather than MATCH
setopt local_options BASH_REMATCH
fi
# 1-based version of BASH_REMATCH. Modifying BASH_REMATCH
# directly causes older versions of Bash to exit
local _BASH_REMATCH="";
local executable=""
# req_argv contains the arguments to the completion
# indexed from 1 (regardless of the shell.) In Bash,
# the zeroth index is empty
local req_argv=()
if [[ -z "${ZSH_VERSION-}" ]]; then
executable=$1
req_argv=( "" "${COMP_WORDS[@]:1}" )
__python_argcomplete_expand_tilde_by_ref executable
else
executable="${words[1]}"
__python_argcomplete_expand_tilde_by_ref executable
req_argv=( "${words[@]:1}" )
fi
local ARGCOMPLETE=0
if [[ "$executable" == python* ]] || [[ "$executable" == pypy* ]]; then
if [[ "${req_argv[1]}" == -m ]]; then
if __python_argcomplete_run "$executable" -m argcomplete._check_module "${req_argv[2]}"; then
ARGCOMPLETE=3
else
return
fi
fi
if [[ $ARGCOMPLETE == 0 ]]; then
local potential_path="${req_argv[1]}"
__python_argcomplete_expand_tilde_by_ref potential_path
if [[ -f "$potential_path" ]] && __python_argcomplete_scan_head_noerr "$potential_path" magic_string; then
req_argv[1]="$potential_path" # not expanded in __python_argcomplete_run
ARGCOMPLETE=2
else
return
fi
fi
elif __python_argcomplete_which "$executable" >/dev/null 2>&1; then
local SCRIPT_NAME=$(__python_argcomplete_which "$executable")
__python_argcomplete_scan_head_noerr "$SCRIPT_NAME" interpreter
if (__python_argcomplete_which pyenv && [[ "$SCRIPT_NAME" = $(pyenv root)/shims/* ]]) >/dev/null 2>&1; then
local SCRIPT_NAME=$(pyenv which "$executable")
fi
if (__python_argcomplete_which asdf && __python_argcomplete_scan_head_noerr "$SCRIPT_NAME" asdf) >/dev/null 2>&1; then
local SCRIPT_NAME=$(asdf which "$executable")
fi
if __python_argcomplete_scan_head_noerr "$SCRIPT_NAME" magic_string; then
ARGCOMPLETE=1
elif __python_argcomplete_scan_head_noerr "$SCRIPT_NAME" interpreter; then
__python_argcomplete_upshift_bash_rematch
local interpreter="${_BASH_REMATCH[2]}"
if [[ -n "${ZSH_VERSION-}" ]]; then
interpreter=($=interpreter)
else
interpreter=($interpreter)
fi
if (__python_argcomplete_scan_head_noerr "$SCRIPT_NAME" easy_install \
&& "${interpreter[@]}" "$(__python_argcomplete_which python-argcomplete-check-easy-install-script)" "$SCRIPT_NAME") >/dev/null 2>&1; then
ARGCOMPLETE=1
elif ([[ "${interpreter[@]}" == *python* ]] || [[ "${interpreter[@]}" == *pypy* ]])\
&& __python_argcomplete_run "${interpreter[@]}" -m argcomplete._check_console_script "$SCRIPT_NAME"; then
ARGCOMPLETE=1
fi
fi
fi
if [[ $ARGCOMPLETE != 0 ]]; then
local IFS=$'\013'
if [[ -n "${ZSH_VERSION-}" ]]; then
local completions
completions=($(IFS="$IFS" \
COMP_LINE="$BUFFER" \
COMP_POINT="$CURSOR" \
_ARGCOMPLETE=$ARGCOMPLETE \
_ARGCOMPLETE_SHELL="zsh" \
_ARGCOMPLETE_SUPPRESS_SPACE=1 \
__python_argcomplete_run "$executable" "${(@)req_argv[1, ${ARGCOMPLETE}-1]}"))
local nosort=()
local nospace=()
if is-at-least 5.8; then
nosort=(-o nosort)
fi
if [[ "${completions-}" =~ ([^\\\\]): && "${BASH_REMATCH[2]}" =~ [=/:] ]]; then
nospace=(-S '')
fi
_describe "$executable" completions "${nosort[@]}" "${nospace[@]}"
else
COMPREPLY=($(IFS="$IFS" \
COMP_LINE="$COMP_LINE" \
COMP_POINT="$COMP_POINT" \
COMP_TYPE="$COMP_TYPE" \
_ARGCOMPLETE_COMP_WORDBREAKS="$COMP_WORDBREAKS" \
_ARGCOMPLETE=$ARGCOMPLETE \
_ARGCOMPLETE_SHELL="bash" \
_ARGCOMPLETE_SUPPRESS_SPACE=1 \
__python_argcomplete_run "$executable" "${req_argv[@]:1:${ARGCOMPLETE}-1}"))
if [[ $? != 0 ]]; then
unset COMPREPLY
elif [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then
compopt -o nospace
fi
fi
elif [[ -n "${ZSH_VERSION-}" ]]; then
_default
else
type -t _completion_loader | grep -q 'function' && _completion_loader "$@"
fi
}
if [[ -z "${ZSH_VERSION-}" ]]; then
complete -o default -o bashdefault -D -F _python_argcomplete_global
else
# -Uz is recommended for the use of functions supplied with the zsh distribution.
# https://unix.stackexchange.com/a/214306
autoload -Uz is-at-least
# If this is being implicitly loaded because we placed it on fpath,
# the comment at the top of this file causes zsh to invoke this script directly,
# so we must explicitly call the global completion function.
# Note $service should only ever be -default- because the comment at the top
# registers this script as the default completer (#compdef -default-).
if [[ $service == -default- ]]; then
_python_argcomplete_global
fi
# If this has been executed directly (e.g. `eval "$(activate-global-python-argcomplete --dest=-)"`)
# we need to explicitly call compdef to register the completion function.
# If we have been implicitly loaded, we still call compdef as a slight optimisation
# (there is no need to execute any top-level code more than once).
compdef _python_argcomplete_global -default-
fi

View file

@ -0,0 +1,137 @@
# Copyright 2012-2023, Andrey Kislyuk and argcomplete contributors.
# Licensed under the Apache License. See https://github.com/kislyuk/argcomplete for more info.
import argparse
import os
import subprocess
def _call(*args, **kwargs):
# TODO: replace "universal_newlines" with "text" once 3.6 support is dropped
kwargs["universal_newlines"] = True
try:
return subprocess.check_output(*args, **kwargs).splitlines()
except subprocess.CalledProcessError:
return []
class BaseCompleter:
"""
This is the base class that all argcomplete completers should subclass.
"""
def __call__(
self, *, prefix: str, action: argparse.Action, parser: argparse.ArgumentParser, parsed_args: argparse.Namespace
) -> None:
raise NotImplementedError("This method should be implemented by a subclass.")
class ChoicesCompleter(BaseCompleter):
def __init__(self, choices):
self.choices = choices
def _convert(self, choice):
if not isinstance(choice, str):
choice = str(choice)
return choice
def __call__(self, **kwargs):
return (self._convert(c) for c in self.choices)
EnvironCompleter = ChoicesCompleter(os.environ)
class FilesCompleter(BaseCompleter):
"""
File completer class, optionally takes a list of allowed extensions
"""
def __init__(self, allowednames=(), directories=True):
# Fix if someone passes in a string instead of a list
if isinstance(allowednames, (str, bytes)):
allowednames = [allowednames]
self.allowednames = [x.lstrip("*").lstrip(".") for x in allowednames]
self.directories = directories
def __call__(self, prefix, **kwargs):
completion = []
if self.allowednames:
if self.directories:
# Using 'bind' in this and the following commands is a workaround to a bug in bash
# that was fixed in bash 5.3 but affects older versions. Environment variables are not treated
# correctly in older versions and calling bind makes them available. For details, see
# https://savannah.gnu.org/support/index.php?111125
files = _call(
["bash", "-c", "bind; compgen -A directory -- '{p}'".format(p=prefix)], stderr=subprocess.DEVNULL
)
completion += [f + "/" for f in files]
for x in self.allowednames:
completion += _call(
["bash", "-c", "bind; compgen -A file -X '!*.{0}' -- '{p}'".format(x, p=prefix)],
stderr=subprocess.DEVNULL,
)
else:
completion += _call(
["bash", "-c", "bind; compgen -A file -- '{p}'".format(p=prefix)], stderr=subprocess.DEVNULL
)
anticomp = _call(
["bash", "-c", "bind; compgen -A directory -- '{p}'".format(p=prefix)], stderr=subprocess.DEVNULL
)
completion = list(set(completion) - set(anticomp))
if self.directories:
completion += [f + "/" for f in anticomp]
return completion
class _FilteredFilesCompleter(BaseCompleter):
def __init__(self, predicate):
"""
Create the completer
A predicate accepts as its only argument a candidate path and either
accepts it or rejects it.
"""
assert predicate, "Expected a callable predicate"
self.predicate = predicate
def __call__(self, prefix, **kwargs):
"""
Provide completions on prefix
"""
target_dir = os.path.dirname(prefix)
try:
names = os.listdir(target_dir or ".")
except Exception:
return # empty iterator
incomplete_part = os.path.basename(prefix)
# Iterate on target_dir entries and filter on given predicate
for name in names:
if not name.startswith(incomplete_part):
continue
candidate = os.path.join(target_dir, name)
if not self.predicate(candidate):
continue
yield candidate + "/" if os.path.isdir(candidate) else candidate
class DirectoriesCompleter(_FilteredFilesCompleter):
def __init__(self):
_FilteredFilesCompleter.__init__(self, predicate=os.path.isdir)
class SuppressCompleter(BaseCompleter):
"""
A completer used to suppress the completion of specific arguments
"""
def __init__(self):
pass
def suppress(self):
"""
Decide if the completion should be suppressed
"""
return True

View file

@ -0,0 +1,2 @@
class ArgcompleteException(Exception):
"Exception raised when the shell argument completion process fails."

View file

@ -0,0 +1,621 @@
# Copyright 2012-2023, Andrey Kislyuk and argcomplete contributors. Licensed under the terms of the
# `Apache License, Version 2.0 <http://www.apache.org/licenses/LICENSE-2.0>`_. Distribution of the LICENSE and NOTICE
# files with source copies of this package and derivative works is **REQUIRED** as specified by the Apache License.
# See https://github.com/kislyuk/argcomplete for more info.
import argparse
import os
import sys
from collections.abc import Mapping
from typing import Callable, Dict, List, Optional, Sequence, TextIO, Union
from . import io as _io
from .completers import BaseCompleter, ChoicesCompleter, FilesCompleter, SuppressCompleter
from .io import debug, mute_stderr
from .lexers import split_line
from .packages._argparse import IntrospectiveArgumentParser, action_is_greedy, action_is_open, action_is_satisfied
safe_actions = {
argparse._StoreAction,
argparse._StoreConstAction,
argparse._StoreTrueAction,
argparse._StoreFalseAction,
argparse._AppendAction,
argparse._AppendConstAction,
argparse._CountAction,
}
def default_validator(completion, prefix):
return completion.startswith(prefix)
class CompletionFinder(object):
"""
Inherit from this class if you wish to override any of the stages below. Otherwise, use
``argcomplete.autocomplete()`` directly (it's a convenience instance of this class). It has the same signature as
:meth:`CompletionFinder.__call__()`.
"""
def __init__(
self,
argument_parser=None,
always_complete_options=True,
exclude=None,
validator=None,
print_suppressed=False,
default_completer=FilesCompleter(),
append_space=None,
):
self._parser = argument_parser
self._formatter = None
self.always_complete_options = always_complete_options
self.exclude = exclude
if validator is None:
validator = default_validator
self.validator = validator
self.print_suppressed = print_suppressed
self.completing = False
self._display_completions: Dict[str, str] = {}
self.default_completer = default_completer
if append_space is None:
append_space = os.environ.get("_ARGCOMPLETE_SUPPRESS_SPACE") != "1"
self.append_space = append_space
def __call__(
self,
argument_parser: argparse.ArgumentParser,
always_complete_options: Union[bool, str] = True,
exit_method: Callable = os._exit,
output_stream: Optional[TextIO] = None,
exclude: Optional[Sequence[str]] = None,
validator: Optional[Callable] = None,
print_suppressed: bool = False,
append_space: Optional[bool] = None,
default_completer: BaseCompleter = FilesCompleter(),
) -> None:
"""
:param argument_parser: The argument parser to autocomplete on
:param always_complete_options:
Controls the autocompletion of option strings if an option string opening character (normally ``-``) has not
been entered. If ``True`` (default), both short (``-x``) and long (``--x``) option strings will be
suggested. If ``False``, no option strings will be suggested. If ``long``, long options and short options
with no long variant will be suggested. If ``short``, short options and long options with no short variant
will be suggested.
:param exit_method:
Method used to stop the program after printing completions. Defaults to :meth:`os._exit`. If you want to
perform a normal exit that calls exit handlers, use :meth:`sys.exit`.
:param exclude: List of strings representing options to be omitted from autocompletion
:param validator:
Function to filter all completions through before returning (called with two string arguments, completion
and prefix; return value is evaluated as a boolean)
:param print_suppressed:
Whether or not to autocomplete options that have the ``help=argparse.SUPPRESS`` keyword argument set.
:param append_space:
Whether to append a space to unique matches. The default is ``True``.
.. note::
If you are not subclassing CompletionFinder to override its behaviors,
use :meth:`argcomplete.autocomplete()` directly. It has the same signature as this method.
Produces tab completions for ``argument_parser``. See module docs for more info.
Argcomplete only executes actions if their class is known not to have side effects. Custom action classes can be
added to argcomplete.safe_actions, if their values are wanted in the ``parsed_args`` completer argument, or
their execution is otherwise desirable.
"""
self.__init__( # type: ignore
argument_parser,
always_complete_options=always_complete_options,
exclude=exclude,
validator=validator,
print_suppressed=print_suppressed,
append_space=append_space,
default_completer=default_completer,
)
if "_ARGCOMPLETE" not in os.environ:
# not an argument completion invocation
return
self._init_debug_stream()
if output_stream is None:
filename = os.environ.get("_ARGCOMPLETE_STDOUT_FILENAME")
if filename is not None:
debug("Using output file {}".format(filename))
output_stream = open(filename, "w")
if output_stream is None:
try:
output_stream = os.fdopen(8, "w")
except Exception:
debug("Unable to open fd 8 for writing, quitting")
exit_method(1)
assert output_stream is not None
ifs = os.environ.get("_ARGCOMPLETE_IFS", "\013")
if len(ifs) != 1:
debug("Invalid value for IFS, quitting [{v}]".format(v=ifs))
exit_method(1)
dfs = os.environ.get("_ARGCOMPLETE_DFS")
if dfs and len(dfs) != 1:
debug("Invalid value for DFS, quitting [{v}]".format(v=dfs))
exit_method(1)
comp_line = os.environ["COMP_LINE"]
comp_point = int(os.environ["COMP_POINT"])
cword_prequote, cword_prefix, cword_suffix, comp_words, last_wordbreak_pos = split_line(comp_line, comp_point)
# _ARGCOMPLETE is set by the shell script to tell us where comp_words
# should start, based on what we're completing.
# 1: <script> [args]
# 2: python <script> [args]
# 3: python -m <module> [args]
start = int(os.environ["_ARGCOMPLETE"]) - 1
comp_words = comp_words[start:]
if cword_prefix and cword_prefix[0] in self._parser.prefix_chars and "=" in cword_prefix:
# Special case for when the current word is "--optional=PARTIAL_VALUE". Give the optional to the parser.
comp_words.append(cword_prefix.split("=", 1)[0])
debug(
"\nLINE: {!r}".format(comp_line),
"\nPOINT: {!r}".format(comp_point),
"\nPREQUOTE: {!r}".format(cword_prequote),
"\nPREFIX: {!r}".format(cword_prefix),
"\nSUFFIX: {!r}".format(cword_suffix),
"\nWORDS:",
comp_words,
)
completions = self._get_completions(comp_words, cword_prefix, cword_prequote, last_wordbreak_pos)
if dfs:
display_completions = {
key: value.replace(ifs, " ") if value else "" for key, value in self._display_completions.items()
}
completions = [dfs.join((key, display_completions.get(key) or "")) for key in completions]
if os.environ.get("_ARGCOMPLETE_SHELL") == "zsh":
completions = [f"{c}:{self._display_completions.get(c)}" for c in completions]
debug("\nReturning completions:", completions)
output_stream.write(ifs.join(completions))
output_stream.flush()
_io.debug_stream.flush()
exit_method(0)
def _init_debug_stream(self):
"""Initialize debug output stream
By default, writes to file descriptor 9, or stderr if that fails.
This can be overridden by derived classes, for example to avoid
clashes with file descriptors being used elsewhere (such as in pytest).
"""
try:
_io.debug_stream = os.fdopen(9, "w")
except Exception:
_io.debug_stream = sys.stderr
debug()
def _get_completions(self, comp_words, cword_prefix, cword_prequote, last_wordbreak_pos):
active_parsers = self._patch_argument_parser()
parsed_args = argparse.Namespace()
self.completing = True
try:
debug("invoking parser with", comp_words[1:])
with mute_stderr():
a = self._parser.parse_known_args(comp_words[1:], namespace=parsed_args)
debug("parsed args:", a)
except BaseException as e:
debug("\nexception", type(e), str(e), "while parsing args")
self.completing = False
if "--" in comp_words:
self.always_complete_options = False
completions = self.collect_completions(active_parsers, parsed_args, cword_prefix)
completions = self.filter_completions(completions)
completions = self.quote_completions(completions, cword_prequote, last_wordbreak_pos)
return completions
def _patch_argument_parser(self):
"""
Since argparse doesn't support much introspection, we monkey-patch it to replace the parse_known_args method and
all actions with hooks that tell us which action was last taken or about to be taken, and let us have the parser
figure out which subparsers need to be activated (then recursively monkey-patch those).
We save all active ArgumentParsers to extract all their possible option names later.
"""
self.active_parsers: List[argparse.ArgumentParser] = []
self.visited_positionals: List[argparse.Action] = []
completer = self
def patch(parser):
completer.visited_positionals.append(parser)
completer.active_parsers.append(parser)
if isinstance(parser, IntrospectiveArgumentParser):
return
classname = "MonkeyPatchedIntrospectiveArgumentParser"
parser.__class__ = type(classname, (IntrospectiveArgumentParser, parser.__class__), {})
for action in parser._actions:
if hasattr(action, "_orig_class"):
continue
# TODO: accomplish this with super
class IntrospectAction(action.__class__): # type: ignore
def __call__(self, parser, namespace, values, option_string=None):
debug("Action stub called on", self)
debug("\targs:", parser, namespace, values, option_string)
debug("\torig class:", self._orig_class)
debug("\torig callable:", self._orig_callable)
if not completer.completing:
self._orig_callable(parser, namespace, values, option_string=option_string)
elif issubclass(self._orig_class, argparse._SubParsersAction):
debug("orig class is a subparsers action: patching and running it")
patch(self._name_parser_map[values[0]])
self._orig_callable(parser, namespace, values, option_string=option_string)
elif self._orig_class in safe_actions:
if not self.option_strings:
completer.visited_positionals.append(self)
self._orig_callable(parser, namespace, values, option_string=option_string)
action._orig_class = action.__class__
action._orig_callable = action.__call__
action.__class__ = IntrospectAction
patch(self._parser)
debug("Active parsers:", self.active_parsers)
debug("Visited positionals:", self.visited_positionals)
return self.active_parsers
def _get_action_help(self, action):
if action.help is None:
return ""
if "%" not in action.help:
return action.help
if self._formatter is None:
self._formatter = self._parser.formatter_class(prog=self._parser.prog)
return self._formatter._expand_help(action)
def _get_subparser_completions(self, parser, cword_prefix):
aliases_by_parser: Dict[argparse.ArgumentParser, List[str]] = {}
for key in parser.choices.keys():
p = parser.choices[key]
aliases_by_parser.setdefault(p, []).append(key)
for action in parser._get_subactions():
for alias in aliases_by_parser[parser.choices[action.dest]]:
if alias.startswith(cword_prefix):
self._display_completions[alias] = self._get_action_help(action)
completions = [subcmd for subcmd in parser.choices.keys() if subcmd.startswith(cword_prefix)]
return completions
def _include_options(self, action, cword_prefix):
if len(cword_prefix) > 0 or self.always_complete_options is True:
return [opt for opt in action.option_strings if opt.startswith(cword_prefix)]
long_opts = [opt for opt in action.option_strings if len(opt) > 2]
short_opts = [opt for opt in action.option_strings if len(opt) <= 2]
if self.always_complete_options == "long":
return long_opts if long_opts else short_opts
elif self.always_complete_options == "short":
return short_opts if short_opts else long_opts
return []
def _get_option_completions(self, parser, cword_prefix):
for action in parser._actions:
if action.option_strings:
for option_string in action.option_strings:
if option_string.startswith(cword_prefix):
self._display_completions[option_string] = self._get_action_help(action)
option_completions = []
for action in parser._actions:
if not self.print_suppressed:
completer = getattr(action, "completer", None)
if isinstance(completer, SuppressCompleter) and completer.suppress():
continue
if action.help == argparse.SUPPRESS:
continue
if not self._action_allowed(action, parser):
continue
if not isinstance(action, argparse._SubParsersAction):
option_completions += self._include_options(action, cword_prefix)
return option_completions
@staticmethod
def _action_allowed(action, parser):
# Logic adapted from take_action in ArgumentParser._parse_known_args
# (members are saved by vendor._argparse.IntrospectiveArgumentParser)
for conflict_action in parser._action_conflicts.get(action, []):
if conflict_action in parser._seen_non_default_actions:
return False
return True
def _complete_active_option(self, parser, next_positional, cword_prefix, parsed_args, completions):
debug("Active actions (L={l}): {a}".format(l=len(parser.active_actions), a=parser.active_actions))
isoptional = cword_prefix and cword_prefix[0] in parser.prefix_chars
optional_prefix = ""
greedy_actions = [x for x in parser.active_actions if action_is_greedy(x, isoptional)]
if greedy_actions:
assert len(greedy_actions) == 1, "expect at most 1 greedy action"
# This means the action will fail to parse if the word under the cursor is not given
# to it, so give it exclusive control over completions (flush previous completions)
debug("Resetting completions because", greedy_actions[0], "must consume the next argument")
self._display_completions = {}
completions = []
elif isoptional:
if "=" in cword_prefix:
# Special case for when the current word is "--optional=PARTIAL_VALUE".
# The completer runs on PARTIAL_VALUE. The prefix is added back to the completions
# (and chopped back off later in quote_completions() by the COMP_WORDBREAKS logic).
optional_prefix, _, cword_prefix = cword_prefix.partition("=")
else:
# Only run completers if current word does not start with - (is not an optional)
return completions
complete_remaining_positionals = False
# Use the single greedy action (if there is one) or all active actions.
for active_action in greedy_actions or parser.active_actions:
if not active_action.option_strings: # action is a positional
if action_is_open(active_action):
# Any positional arguments after this may slide down into this action
# if more arguments are added (since the user may not be done yet),
# so it is extremely difficult to tell which completers to run.
# Running all remaining completers will probably show more than the user wants
# but it also guarantees we won't miss anything.
complete_remaining_positionals = True
if not complete_remaining_positionals:
if action_is_satisfied(active_action) and not action_is_open(active_action):
debug("Skipping", active_action)
continue
debug("Activating completion for", active_action, active_action._orig_class)
# completer = getattr(active_action, "completer", DefaultCompleter())
completer = getattr(active_action, "completer", None)
if completer is None:
if active_action.choices is not None and not isinstance(active_action, argparse._SubParsersAction):
completer = ChoicesCompleter(active_action.choices)
elif not isinstance(active_action, argparse._SubParsersAction):
completer = self.default_completer
if completer:
if isinstance(completer, SuppressCompleter) and completer.suppress():
continue
if callable(completer):
completer_output = completer(
prefix=cword_prefix, action=active_action, parser=parser, parsed_args=parsed_args
)
if isinstance(completer_output, Mapping):
for completion, description in completer_output.items():
if self.validator(completion, cword_prefix):
completions.append(completion)
self._display_completions[completion] = description
else:
for completion in completer_output:
if self.validator(completion, cword_prefix):
completions.append(completion)
if isinstance(completer, ChoicesCompleter):
self._display_completions[completion] = self._get_action_help(active_action)
else:
self._display_completions[completion] = ""
else:
debug("Completer is not callable, trying the readline completer protocol instead")
for i in range(9999):
next_completion = completer.complete(cword_prefix, i) # type: ignore
if next_completion is None:
break
if self.validator(next_completion, cword_prefix):
self._display_completions[next_completion] = ""
completions.append(next_completion)
if optional_prefix:
completions = [optional_prefix + "=" + completion for completion in completions]
debug("Completions:", completions)
return completions
def collect_completions(
self, active_parsers: List[argparse.ArgumentParser], parsed_args: argparse.Namespace, cword_prefix: str
) -> List[str]:
"""
Visits the active parsers and their actions, executes their completers or introspects them to collect their
option strings. Returns the resulting completions as a list of strings.
This method is exposed for overriding in subclasses; there is no need to use it directly.
"""
completions: List[str] = []
debug("all active parsers:", active_parsers)
active_parser = active_parsers[-1]
debug("active_parser:", active_parser)
if self.always_complete_options or (len(cword_prefix) > 0 and cword_prefix[0] in active_parser.prefix_chars):
completions += self._get_option_completions(active_parser, cword_prefix)
debug("optional options:", completions)
next_positional = self._get_next_positional()
debug("next_positional:", next_positional)
if isinstance(next_positional, argparse._SubParsersAction):
completions += self._get_subparser_completions(next_positional, cword_prefix)
completions = self._complete_active_option(
active_parser, next_positional, cword_prefix, parsed_args, completions
)
debug("active options:", completions)
debug("display completions:", self._display_completions)
return completions
def _get_next_positional(self):
"""
Get the next positional action if it exists.
"""
active_parser = self.active_parsers[-1]
last_positional = self.visited_positionals[-1]
all_positionals = active_parser._get_positional_actions()
if not all_positionals:
return None
if active_parser == last_positional:
return all_positionals[0]
i = 0
for i in range(len(all_positionals)):
if all_positionals[i] == last_positional:
break
if i + 1 < len(all_positionals):
return all_positionals[i + 1]
return None
def filter_completions(self, completions: List[str]) -> List[str]:
"""
De-duplicates completions and excludes those specified by ``exclude``.
Returns the filtered completions as a list.
This method is exposed for overriding in subclasses; there is no need to use it directly.
"""
filtered_completions = []
for completion in completions:
if self.exclude is not None:
if completion in self.exclude:
continue
if completion not in filtered_completions:
filtered_completions.append(completion)
return filtered_completions
def quote_completions(
self, completions: List[str], cword_prequote: str, last_wordbreak_pos: Optional[int]
) -> List[str]:
"""
If the word under the cursor started with a quote (as indicated by a nonempty ``cword_prequote``), escapes
occurrences of that quote character in the completions, and adds the quote to the beginning of each completion.
Otherwise, escapes all characters that bash splits words on (``COMP_WORDBREAKS``), and removes portions of
completions before the first colon if (``COMP_WORDBREAKS``) contains a colon.
If there is only one completion, and it doesn't end with a **continuation character** (``/``, ``:``, or ``=``),
adds a space after the completion.
This method is exposed for overriding in subclasses; there is no need to use it directly.
"""
special_chars = "\\"
# If the word under the cursor was quoted, escape the quote char.
# Otherwise, escape all special characters and specially handle all COMP_WORDBREAKS chars.
if cword_prequote == "":
# Bash mangles completions which contain characters in COMP_WORDBREAKS.
# This workaround has the same effect as __ltrim_colon_completions in bash_completion
# (extended to characters other than the colon).
if last_wordbreak_pos is not None:
completions = [c[last_wordbreak_pos + 1 :] for c in completions]
special_chars += "();<>|&!`$* \t\n\"'"
elif cword_prequote == '"':
special_chars += '"`$!'
if os.environ.get("_ARGCOMPLETE_SHELL") in ("tcsh", "fish"):
# tcsh and fish escapes special characters itself.
special_chars = ""
elif cword_prequote == "'":
# Nothing can be escaped in single quotes, so we need to close
# the string, escape the single quote, then open a new string.
special_chars = ""
completions = [c.replace("'", r"'\''") for c in completions]
# PowerShell uses ` as escape character.
if os.environ.get("_ARGCOMPLETE_SHELL") == "powershell":
escape_char = '`'
special_chars = special_chars.replace('`', '')
else:
escape_char = "\\"
if os.environ.get("_ARGCOMPLETE_SHELL") == "zsh":
# zsh uses colon as a separator between a completion and its description.
special_chars += ":"
escaped_completions = []
for completion in completions:
escaped_completion = completion
for char in special_chars:
escaped_completion = escaped_completion.replace(char, escape_char + char)
escaped_completions.append(escaped_completion)
if completion in self._display_completions:
self._display_completions[escaped_completion] = self._display_completions[completion]
if self.append_space:
# Similar functionality in bash was previously turned off by supplying the "-o nospace" option to complete.
# Now it is conditionally disabled using "compopt -o nospace" if the match ends in a continuation character.
# This code is retained for environments where this isn't done natively.
continuation_chars = "=/:"
if len(escaped_completions) == 1 and escaped_completions[0][-1] not in continuation_chars:
if cword_prequote == "":
escaped_completions[0] += " "
return escaped_completions
def rl_complete(self, text, state):
"""
Alternate entry point for using the argcomplete completer in a readline-based REPL. See also
`rlcompleter <https://docs.python.org/3/library/rlcompleter.html#completer-objects>`_.
Usage:
.. code-block:: python
import argcomplete, argparse, readline
parser = argparse.ArgumentParser()
...
completer = argcomplete.CompletionFinder(parser)
readline.set_completer_delims("")
readline.set_completer(completer.rl_complete)
readline.parse_and_bind("tab: complete")
result = input("prompt> ")
"""
if state == 0:
cword_prequote, cword_prefix, cword_suffix, comp_words, first_colon_pos = split_line(text)
comp_words.insert(0, sys.argv[0])
matches = self._get_completions(comp_words, cword_prefix, cword_prequote, first_colon_pos)
self._rl_matches = [text + match[len(cword_prefix) :] for match in matches]
if state < len(self._rl_matches):
return self._rl_matches[state]
else:
return None
def get_display_completions(self):
"""
This function returns a mapping of completions to their help strings for displaying to the user.
"""
return self._display_completions
class ExclusiveCompletionFinder(CompletionFinder):
@staticmethod
def _action_allowed(action, parser):
if not CompletionFinder._action_allowed(action, parser):
return False
append_classes = (argparse._AppendAction, argparse._AppendConstAction)
if action._orig_class in append_classes:
return True
if action not in parser._seen_non_default_actions:
return True
return False

View file

@ -0,0 +1,42 @@
import contextlib
import os
import sys
_DEBUG = "_ARC_DEBUG" in os.environ
debug_stream = sys.stderr
def debug(*args):
if _DEBUG:
print(file=debug_stream, *args)
@contextlib.contextmanager
def mute_stdout():
stdout = sys.stdout
sys.stdout = open(os.devnull, "w")
try:
yield
finally:
sys.stdout = stdout
@contextlib.contextmanager
def mute_stderr():
stderr = sys.stderr
sys.stderr = open(os.devnull, "w")
try:
yield
finally:
sys.stderr.close()
sys.stderr = stderr
def warn(*args):
"""
Prints **args** to standard error when running completions. This will interrupt the user's command line interaction;
use it to indicate an error condition that is preventing your completer from working.
"""
print(file=debug_stream)
print(file=debug_stream, *args)

View file

@ -0,0 +1,57 @@
import os
from .exceptions import ArgcompleteException
from .io import debug
from .packages import _shlex
def split_line(line, point=None):
if point is None:
point = len(line)
line = line[:point]
lexer = _shlex.shlex(line, posix=True)
lexer.whitespace_split = True
lexer.wordbreaks = os.environ.get("_ARGCOMPLETE_COMP_WORDBREAKS", "")
words = []
def split_word(word):
# TODO: make this less ugly
point_in_word = len(word) + point - lexer.instream.tell()
if isinstance(lexer.state, (str, bytes)) and lexer.state in lexer.whitespace:
point_in_word += 1
if point_in_word > len(word):
debug("In trailing whitespace")
words.append(word)
word = ""
prefix, suffix = word[:point_in_word], word[point_in_word:]
prequote = ""
# posix
if lexer.state is not None and lexer.state in lexer.quotes:
prequote = lexer.state
# non-posix
# if len(prefix) > 0 and prefix[0] in lexer.quotes:
# prequote, prefix = prefix[0], prefix[1:]
return prequote, prefix, suffix, words, lexer.last_wordbreak_pos
while True:
try:
word = lexer.get_token()
if word == lexer.eof:
# TODO: check if this is ever unsafe
# raise ArgcompleteException("Unexpected end of input")
return "", "", "", words, None
if lexer.instream.tell() >= point:
debug("word", word, "split, lexer state: '{s}'".format(s=lexer.state))
return split_word(word)
words.append(word)
except ValueError:
debug("word", lexer.token, "split (lexer stopped, state: '{s}')".format(s=lexer.state))
if lexer.instream.tell() >= point:
return split_word(lexer.token)
else:
msg = (
"Unexpected internal state. "
"Please report this bug at https://github.com/kislyuk/argcomplete/issues."
)
raise ArgcompleteException(msg)

View file

@ -0,0 +1,342 @@
# Copyright 2012-2023, Andrey Kislyuk and argcomplete contributors. Licensed under the terms of the
# `Apache License, Version 2.0 <http://www.apache.org/licenses/LICENSE-2.0>`_. Distribution of the LICENSE and NOTICE
# files with source copies of this package and derivative works is **REQUIRED** as specified by the Apache License.
# See https://github.com/kislyuk/argcomplete for more info.
# This file contains argparse introspection utilities used in the course of argcomplete execution.
from argparse import (
ONE_OR_MORE,
OPTIONAL,
PARSER,
REMAINDER,
SUPPRESS,
ZERO_OR_MORE,
Action,
ArgumentError,
ArgumentParser,
_get_action_name,
_SubParsersAction,
)
from gettext import gettext
from typing import Dict, List, Set, Tuple
_num_consumed_args: Dict[Action, int] = {}
def action_is_satisfied(action):
'''Returns False if the parse would raise an error if no more arguments are given to this action, True otherwise.'''
num_consumed_args = _num_consumed_args.get(action, 0)
if action.nargs in [OPTIONAL, ZERO_OR_MORE, REMAINDER]:
return True
if action.nargs == ONE_OR_MORE:
return num_consumed_args >= 1
if action.nargs == PARSER:
# Not sure what this should be, but this previously always returned False
# so at least this won't break anything that wasn't already broken.
return False
if action.nargs is None:
return num_consumed_args == 1
assert isinstance(action.nargs, int), 'failed to handle a possible nargs value: %r' % action.nargs
return num_consumed_args == action.nargs
def action_is_open(action):
'''Returns True if action could consume more arguments (i.e., its pattern is open).'''
num_consumed_args = _num_consumed_args.get(action, 0)
if action.nargs in [ZERO_OR_MORE, ONE_OR_MORE, PARSER, REMAINDER]:
return True
if action.nargs == OPTIONAL or action.nargs is None:
return num_consumed_args == 0
assert isinstance(action.nargs, int), 'failed to handle a possible nargs value: %r' % action.nargs
return num_consumed_args < action.nargs
def action_is_greedy(action, isoptional=False):
'''Returns True if action will necessarily consume the next argument.
isoptional indicates whether the argument is an optional (starts with -).
'''
num_consumed_args = _num_consumed_args.get(action, 0)
if action.option_strings:
if not isoptional and not action_is_satisfied(action):
return True
return action.nargs == REMAINDER
else:
return action.nargs == REMAINDER and num_consumed_args >= 1
class IntrospectiveArgumentParser(ArgumentParser):
'''The following is a verbatim copy of ArgumentParser._parse_known_args (Python 2.7.3),
except for the lines that contain the string "Added by argcomplete".
'''
def _parse_known_args(self, arg_strings, namespace, intermixed=False, **kwargs):
_num_consumed_args.clear() # Added by argcomplete
self._argcomplete_namespace = namespace
self.active_actions: List[Action] = [] # Added by argcomplete
# replace arg strings that are file references
if self.fromfile_prefix_chars is not None:
arg_strings = self._read_args_from_files(arg_strings)
# map all mutually exclusive arguments to the other arguments
# they can't occur with
action_conflicts: Dict[Action, List[Action]] = {}
self._action_conflicts = action_conflicts # Added by argcomplete
for mutex_group in self._mutually_exclusive_groups:
group_actions = mutex_group._group_actions
for i, mutex_action in enumerate(mutex_group._group_actions):
conflicts = action_conflicts.setdefault(mutex_action, [])
conflicts.extend(group_actions[:i])
conflicts.extend(group_actions[i + 1 :])
# find all option indices, and determine the arg_string_pattern
# which has an 'O' if there is an option at an index,
# an 'A' if there is an argument, or a '-' if there is a '--'
option_string_indices = {}
arg_string_pattern_parts = []
arg_strings_iter = iter(arg_strings)
for i, arg_string in enumerate(arg_strings_iter):
# all args after -- are non-options
if arg_string == '--':
arg_string_pattern_parts.append('-')
for arg_string in arg_strings_iter:
arg_string_pattern_parts.append('A')
# otherwise, add the arg to the arg strings
# and note the index if it was an option
else:
option_tuple = self._parse_optional(arg_string)
if option_tuple is None:
pattern = 'A'
else:
option_string_indices[i] = option_tuple
pattern = 'O'
arg_string_pattern_parts.append(pattern)
# join the pieces together to form the pattern
arg_strings_pattern = ''.join(arg_string_pattern_parts)
# converts arg strings to the appropriate and then takes the action
seen_actions: Set[Action] = set()
seen_non_default_actions: Set[Action] = set()
self._seen_non_default_actions = seen_non_default_actions # Added by argcomplete
def take_action(action, argument_strings, option_string=None):
seen_actions.add(action)
argument_values = self._get_values(action, argument_strings)
# error if this argument is not allowed with other previously
# seen arguments, assuming that actions that use the default
# value don't really count as "present"
if argument_values is not action.default:
seen_non_default_actions.add(action)
for conflict_action in action_conflicts.get(action, []):
if conflict_action in seen_non_default_actions:
msg = gettext('not allowed with argument %s')
action_name = _get_action_name(conflict_action)
raise ArgumentError(action, msg % action_name)
# take the action if we didn't receive a SUPPRESS value
# (e.g. from a default)
if argument_values is not SUPPRESS or isinstance(action, _SubParsersAction):
try:
action(self, namespace, argument_values, option_string)
except BaseException:
# Begin added by argcomplete
# When a subparser action is taken and fails due to incomplete arguments, it does not merge the
# contents of its parsed namespace into the parent namespace. Do that here to allow completers to
# access the partially parsed arguments for the subparser.
if isinstance(action, _SubParsersAction):
subnamespace = action._name_parser_map[argument_values[0]]._argcomplete_namespace
for key, value in vars(subnamespace).items():
setattr(namespace, key, value)
# End added by argcomplete
raise
# function to convert arg_strings into an optional action
def consume_optional(start_index):
# get the optional identified at this index
option_tuple = option_string_indices[start_index]
if isinstance(option_tuple, list): # Python 3.12.7+
option_tuple = option_tuple[0]
if len(option_tuple) == 3:
action, option_string, explicit_arg = option_tuple
else: # Python 3.11.9+, 3.12.3+, 3.13+
action, option_string, _, explicit_arg = option_tuple
# identify additional optionals in the same arg string
# (e.g. -xyz is the same as -x -y -z if no args are required)
match_argument = self._match_argument
action_tuples: List[Tuple[Action, List[str], str]] = []
while True:
# if we found no optional action, skip it
if action is None:
extras.append(arg_strings[start_index])
return start_index + 1
# if there is an explicit argument, try to match the
# optional's string arguments to only this
if explicit_arg is not None:
arg_count = match_argument(action, 'A')
# if the action is a single-dash option and takes no
# arguments, try to parse more single-dash options out
# of the tail of the option string
chars = self.prefix_chars
if arg_count == 0 and option_string[1] not in chars:
action_tuples.append((action, [], option_string))
char = option_string[0]
option_string = char + explicit_arg[0]
new_explicit_arg = explicit_arg[1:] or None
optionals_map = self._option_string_actions
if option_string in optionals_map:
action = optionals_map[option_string]
explicit_arg = new_explicit_arg
else:
msg = gettext('ignored explicit argument %r')
raise ArgumentError(action, msg % explicit_arg)
# if the action expect exactly one argument, we've
# successfully matched the option; exit the loop
elif arg_count == 1:
stop = start_index + 1
args = [explicit_arg]
action_tuples.append((action, args, option_string))
break
# error if a double-dash option did not use the
# explicit argument
else:
msg = gettext('ignored explicit argument %r')
raise ArgumentError(action, msg % explicit_arg)
# if there is no explicit argument, try to match the
# optional's string arguments with the following strings
# if successful, exit the loop
else:
start = start_index + 1
selected_patterns = arg_strings_pattern[start:]
self.active_actions = [action] # Added by argcomplete
_num_consumed_args[action] = 0 # Added by argcomplete
arg_count = match_argument(action, selected_patterns)
stop = start + arg_count
args = arg_strings[start:stop]
# Begin added by argcomplete
# If the pattern is not open (e.g. no + at the end), remove the action from active actions (since
# it wouldn't be able to consume any more args)
_num_consumed_args[action] = len(args)
if not action_is_open(action):
self.active_actions.remove(action)
# End added by argcomplete
action_tuples.append((action, args, option_string))
break
# add the Optional to the list and return the index at which
# the Optional's string args stopped
assert action_tuples
for action, args, option_string in action_tuples:
take_action(action, args, option_string)
return stop
# the list of Positionals left to be parsed; this is modified
# by consume_positionals()
positionals = self._get_positional_actions()
# function to convert arg_strings into positional actions
def consume_positionals(start_index):
# match as many Positionals as possible
match_partial = self._match_arguments_partial
selected_pattern = arg_strings_pattern[start_index:]
arg_counts = match_partial(positionals, selected_pattern)
# slice off the appropriate arg strings for each Positional
# and add the Positional and its args to the list
for action, arg_count in zip(positionals, arg_counts): # Added by argcomplete
self.active_actions.append(action) # Added by argcomplete
for action, arg_count in zip(positionals, arg_counts):
args = arg_strings[start_index : start_index + arg_count]
start_index += arg_count
_num_consumed_args[action] = len(args) # Added by argcomplete
take_action(action, args)
# slice off the Positionals that we just parsed and return the
# index at which the Positionals' string args stopped
positionals[:] = positionals[len(arg_counts) :]
return start_index
# consume Positionals and Optionals alternately, until we have
# passed the last option string
extras = []
start_index = 0
if option_string_indices:
max_option_string_index = max(option_string_indices)
else:
max_option_string_index = -1
while start_index <= max_option_string_index:
# consume any Positionals preceding the next option
next_option_string_index = min([index for index in option_string_indices if index >= start_index])
if start_index != next_option_string_index:
positionals_end_index = consume_positionals(start_index)
# only try to parse the next optional if we didn't consume
# the option string during the positionals parsing
if positionals_end_index > start_index:
start_index = positionals_end_index
continue
else:
start_index = positionals_end_index
# if we consumed all the positionals we could and we're not
# at the index of an option string, there were extra arguments
if start_index not in option_string_indices:
strings = arg_strings[start_index:next_option_string_index]
extras.extend(strings)
start_index = next_option_string_index
# consume the next optional and any arguments for it
start_index = consume_optional(start_index)
# consume any positionals following the last Optional
stop_index = consume_positionals(start_index)
# if we didn't consume all the argument strings, there were extras
extras.extend(arg_strings[stop_index:])
# if we didn't use all the Positional objects, there were too few
# arg strings supplied.
if positionals:
self.active_actions.append(positionals[0]) # Added by argcomplete
self.error(gettext('too few arguments'))
# make sure all required actions were present
for action in self._actions:
if action.required:
if action not in seen_actions:
name = _get_action_name(action)
self.error(gettext('argument %s is required') % name)
# make sure all required groups had one option present
for group in self._mutually_exclusive_groups:
if group.required:
for action in group._group_actions:
if action in seen_non_default_actions:
break
# if no actions were used, report the error
else:
names = [
str(_get_action_name(action)) for action in group._group_actions if action.help is not SUPPRESS
]
msg = gettext('one of the arguments %s is required')
self.error(msg % ' '.join(names))
# return the updated namespace and the extra arguments
return namespace, extras

View file

@ -0,0 +1,313 @@
# This copy of shlex.py from Python 3.6 is distributed with argcomplete.
# It contains only the shlex class, with modifications as noted.
"""A lexical analyzer class for simple shell-like syntaxes."""
# Module and documentation by Eric S. Raymond, 21 Dec 1998
# Input stacking and error message cleanup added by ESR, March 2000
# push_source() and pop_source() made explicit by ESR, January 2001.
# Posix compliance, split(), string arguments, and
# iterator interface by Gustavo Niemeyer, April 2003.
# changes to tokenize more like Posix shells by Vinay Sajip, July 2016.
import os
import sys
from collections import deque
from io import StringIO
from typing import Optional
class shlex:
"A lexical analyzer class for simple shell-like syntaxes."
def __init__(self, instream=None, infile=None, posix=False, punctuation_chars=False):
# Modified by argcomplete: 2/3 compatibility
if isinstance(instream, str):
instream = StringIO(instream)
if instream is not None:
self.instream = instream
self.infile = infile
else:
self.instream = sys.stdin
self.infile = None
self.posix = posix
if posix:
self.eof = None
else:
self.eof = ''
self.commenters = '#'
self.wordchars = 'abcdfeghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
# Modified by argcomplete: 2/3 compatibility
# if self.posix:
# self.wordchars += ('ßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ'
# 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ')
self.whitespace = ' \t\r\n'
self.whitespace_split = False
self.quotes = '\'"'
self.escape = '\\'
self.escapedquotes = '"'
self.state: Optional[str] = ' '
self.pushback: deque = deque()
self.lineno = 1
self.debug = 0
self.token = ''
self.filestack: deque = deque()
self.source = None
if not punctuation_chars:
punctuation_chars = ''
elif punctuation_chars is True:
punctuation_chars = '();<>|&'
self.punctuation_chars = punctuation_chars
if punctuation_chars:
# _pushback_chars is a push back queue used by lookahead logic
self._pushback_chars: deque = deque()
# these chars added because allowed in file names, args, wildcards
self.wordchars += '~-./*?='
# remove any punctuation chars from wordchars
t = self.wordchars.maketrans(dict.fromkeys(punctuation_chars))
self.wordchars = self.wordchars.translate(t)
# Modified by argcomplete: Record last wordbreak position
self.last_wordbreak_pos = None
self.wordbreaks = ''
def push_token(self, tok):
"Push a token onto the stack popped by the get_token method"
if self.debug >= 1:
print("shlex: pushing token " + repr(tok))
self.pushback.appendleft(tok)
def push_source(self, newstream, newfile=None):
"Push an input source onto the lexer's input source stack."
# Modified by argcomplete: 2/3 compatibility
if isinstance(newstream, str):
newstream = StringIO(newstream)
self.filestack.appendleft((self.infile, self.instream, self.lineno))
self.infile = newfile
self.instream = newstream
self.lineno = 1
if self.debug:
if newfile is not None:
print('shlex: pushing to file %s' % (self.infile,))
else:
print('shlex: pushing to stream %s' % (self.instream,))
def pop_source(self):
"Pop the input source stack."
self.instream.close()
(self.infile, self.instream, self.lineno) = self.filestack.popleft()
if self.debug:
print('shlex: popping to %s, line %d' % (self.instream, self.lineno))
self.state = ' '
def get_token(self):
"Get a token from the input stream (or from stack if it's nonempty)"
if self.pushback:
tok = self.pushback.popleft()
if self.debug >= 1:
print("shlex: popping token " + repr(tok))
return tok
# No pushback. Get a token.
raw = self.read_token()
# Handle inclusions
if self.source is not None:
while raw == self.source:
spec = self.sourcehook(self.read_token())
if spec:
(newfile, newstream) = spec
self.push_source(newstream, newfile)
raw = self.get_token()
# Maybe we got EOF instead?
while raw == self.eof:
if not self.filestack:
return self.eof
else:
self.pop_source()
raw = self.get_token()
# Neither inclusion nor EOF
if self.debug >= 1:
if raw != self.eof:
print("shlex: token=" + repr(raw))
else:
print("shlex: token=EOF")
return raw
def read_token(self):
quoted = False
escapedstate = ' '
while True:
if self.punctuation_chars and self._pushback_chars:
nextchar = self._pushback_chars.pop()
else:
nextchar = self.instream.read(1)
if nextchar == '\n':
self.lineno += 1
if self.debug >= 3:
print("shlex: in state %r I see character: %r" % (self.state, nextchar))
if self.state is None:
self.token = '' # past end of file
break
elif self.state == ' ':
if not nextchar:
self.state = None # end of file
break
elif nextchar in self.whitespace:
if self.debug >= 2:
print("shlex: I see whitespace in whitespace state")
if self.token or (self.posix and quoted):
break # emit current token
else:
continue
elif nextchar in self.commenters:
self.instream.readline()
self.lineno += 1
elif self.posix and nextchar in self.escape:
escapedstate = 'a'
self.state = nextchar
elif nextchar in self.wordchars:
self.token = nextchar
self.state = 'a'
elif nextchar in self.punctuation_chars:
self.token = nextchar
self.state = 'c'
elif nextchar in self.quotes:
if not self.posix:
self.token = nextchar
self.state = nextchar
elif self.whitespace_split:
self.token = nextchar
self.state = 'a'
# Modified by argcomplete: Record last wordbreak position
if nextchar in self.wordbreaks:
self.last_wordbreak_pos = len(self.token) - 1
else:
self.token = nextchar
if self.token or (self.posix and quoted):
break # emit current token
else:
continue
elif self.state in self.quotes:
quoted = True
if not nextchar: # end of file
if self.debug >= 2:
print("shlex: I see EOF in quotes state")
# XXX what error should be raised here?
raise ValueError("No closing quotation")
if nextchar == self.state:
if not self.posix:
self.token += nextchar
self.state = ' '
break
else:
self.state = 'a'
elif self.posix and nextchar in self.escape and self.state in self.escapedquotes:
escapedstate = self.state
self.state = nextchar
else:
self.token += nextchar
elif self.state in self.escape:
if not nextchar: # end of file
if self.debug >= 2:
print("shlex: I see EOF in escape state")
# XXX what error should be raised here?
raise ValueError("No escaped character")
# In posix shells, only the quote itself or the escape
# character may be escaped within quotes.
if escapedstate in self.quotes and nextchar != self.state and nextchar != escapedstate:
self.token += self.state
self.token += nextchar
self.state = escapedstate
elif self.state in ('a', 'c'):
if not nextchar:
self.state = None # end of file
break
elif nextchar in self.whitespace:
if self.debug >= 2:
print("shlex: I see whitespace in word state")
self.state = ' '
if self.token or (self.posix and quoted):
break # emit current token
else:
continue
elif nextchar in self.commenters:
self.instream.readline()
self.lineno += 1
if self.posix:
self.state = ' '
if self.token or (self.posix and quoted):
break # emit current token
else:
continue
elif self.posix and nextchar in self.quotes:
self.state = nextchar
elif self.posix and nextchar in self.escape:
escapedstate = 'a'
self.state = nextchar
elif self.state == 'c':
if nextchar in self.punctuation_chars:
self.token += nextchar
else:
if nextchar not in self.whitespace:
self._pushback_chars.append(nextchar)
self.state = ' '
break
elif nextchar in self.wordchars or nextchar in self.quotes or self.whitespace_split:
self.token += nextchar
# Modified by argcomplete: Record last wordbreak position
if nextchar in self.wordbreaks:
self.last_wordbreak_pos = len(self.token) - 1
else:
if self.punctuation_chars:
self._pushback_chars.append(nextchar)
else:
self.pushback.appendleft(nextchar)
if self.debug >= 2:
print("shlex: I see punctuation in word state")
self.state = ' '
if self.token or (self.posix and quoted):
break # emit current token
else:
continue
result: Optional[str] = self.token
self.token = ''
if self.posix and not quoted and result == '':
result = None
if self.debug > 1:
if result:
print("shlex: raw token=" + repr(result))
else:
print("shlex: raw token=EOF")
# Modified by argcomplete: Record last wordbreak position
if self.state == ' ':
self.last_wordbreak_pos = None
return result
def sourcehook(self, newfile):
"Hook called on a filename to be sourced."
if newfile[0] == '"':
newfile = newfile[1:-1]
# This implements cpp-like semantics for relative-path inclusion.
# Modified by argcomplete: 2/3 compatibility
if isinstance(self.infile, str) and not os.path.isabs(newfile):
newfile = os.path.join(os.path.dirname(self.infile), newfile)
return (newfile, open(newfile, "r"))
def error_leader(self, infile=None, lineno=None):
"Emit a C-compiler-like, Emacs-friendly error-message leader."
if infile is None:
infile = self.infile
if lineno is None:
lineno = self.lineno
return "\"%s\", line %d: " % (infile, lineno)
def __iter__(self):
return self
def __next__(self):
token = self.get_token()
if token == self.eof:
raise StopIteration
return token
# Modified by argcomplete: 2/3 compatibility
next = __next__

View file

@ -0,0 +1,193 @@
#!/usr/bin/env python3
# PYTHON_ARGCOMPLETE_OK
# Copyright 2012-2023, Andrey Kislyuk and argcomplete contributors.
# Licensed under the Apache License. See https://github.com/kislyuk/argcomplete for more info.
"""
Activate the generic bash-completion script or zsh completion autoload function for the argcomplete module.
"""
import argparse
import os
import shutil
import site
import subprocess
import sys
import argcomplete
# PEP 366
__package__ = "argcomplete.scripts"
zsh_shellcode = """
# Begin added by argcomplete
fpath=( {zsh_fpath} "${{fpath[@]}}" )
# End added by argcomplete
"""
bash_shellcode = """
# Begin added by argcomplete
source "{activator}"
# End added by argcomplete
"""
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("-y", "--yes", help="automatically answer yes for all questions", action="store_true")
parser.add_argument("--dest", help='Specify the shell completion modules directory to install into, or "-" for stdout')
parser.add_argument("--user", help="Install into user directory", action="store_true")
argcomplete.autocomplete(parser)
args = None
def get_local_dir():
try:
return subprocess.check_output(["brew", "--prefix"]).decode().strip()
except (FileNotFoundError, subprocess.CalledProcessError):
return "/usr/local"
def get_zsh_system_dir():
return f"{get_local_dir()}/share/zsh/site-functions"
def get_bash_system_dir():
if "BASH_COMPLETION_COMPAT_DIR" in os.environ:
return os.environ["BASH_COMPLETION_COMPAT_DIR"]
elif sys.platform == "darwin":
return f"{get_local_dir()}/etc/bash_completion.d" # created by homebrew
else:
return "/etc/bash_completion.d" # created by bash-completion
def get_activator_dir():
return os.path.join(os.path.abspath(os.path.dirname(argcomplete.__file__)), "bash_completion.d")
def get_activator_path():
return os.path.join(get_activator_dir(), "_python-argcomplete")
def install_to_destination(dest):
activator = get_activator_path()
if dest == "-":
with open(activator) as fh:
sys.stdout.write(fh.read())
return
destdir = os.path.dirname(dest)
if not os.path.exists(destdir):
try:
os.makedirs(destdir, exist_ok=True)
except Exception as e:
parser.error(
f"path {destdir} does not exist and could not be created: {e}. Please run this command using sudo, or see --help for more options."
)
try:
print(f"Installing {activator} to {dest}...", file=sys.stderr)
shutil.copy(activator, dest)
print("Installed.", file=sys.stderr)
except Exception as e:
parser.error(
f"while installing to {dest}: {e}. Please run this command using sudo, or see --help for more options."
)
def get_consent():
assert args is not None
if args.yes is True:
return True
while True:
res = input("OK to proceed? [y/n] ")
if res.lower() not in {"y", "n", "yes", "no"}:
print('Please answer "yes" or "no".', file=sys.stderr)
elif res.lower() in {"y", "yes"}:
return True
else:
return False
def append_to_config_file(path, shellcode):
if os.path.exists(path):
with open(path, 'r') as fh:
if shellcode in fh.read():
print(f"The code already exists in the file {path}.", file=sys.stderr)
return
print(f"argcomplete needs to append to the file {path}. The following code will be appended:", file=sys.stderr)
for line in shellcode.splitlines():
print(">", line, file=sys.stderr)
if not get_consent():
print("Not added.", file=sys.stderr)
return
print(f"Adding shellcode to {path}...", file=sys.stderr)
with open(path, "a") as fh:
fh.write(shellcode)
print("Added.", file=sys.stderr)
def link_zsh_user_rcfile(zsh_fpath=None):
zsh_rcfile = os.path.join(os.path.expanduser(os.environ.get("ZDOTDIR", "~")), ".zshenv")
append_to_config_file(zsh_rcfile, zsh_shellcode.format(zsh_fpath=zsh_fpath or get_activator_dir()))
def link_bash_user_rcfile():
bash_completion_user_file = os.path.expanduser("~/.bash_completion")
append_to_config_file(bash_completion_user_file, bash_shellcode.format(activator=get_activator_path()))
def link_user_rcfiles():
# TODO: warn if running as superuser
link_zsh_user_rcfile()
link_bash_user_rcfile()
def add_zsh_system_dir_to_fpath_for_user():
if "zsh" not in os.environ.get("SHELL", ""):
return
try:
zsh_system_dir = get_zsh_system_dir()
fpath_output = subprocess.check_output([os.environ["SHELL"], "-c", 'printf "%s\n" "${fpath[@]}"'])
for fpath in fpath_output.decode().splitlines():
if fpath == zsh_system_dir:
return
link_zsh_user_rcfile(zsh_fpath=zsh_system_dir)
except (FileNotFoundError, subprocess.CalledProcessError):
pass
def main():
global args
args = parser.parse_args()
destinations = []
if args.dest:
if args.dest != "-" and not os.path.exists(args.dest):
parser.error(f"directory {args.dest} was specified via --dest, but it does not exist")
destinations.append(args.dest)
elif site.ENABLE_USER_SITE and site.USER_SITE and site.USER_SITE in argcomplete.__file__:
print(
"Argcomplete was installed in the user site local directory. Defaulting to user installation.",
file=sys.stderr,
)
link_user_rcfiles()
elif sys.prefix != sys.base_prefix:
print("Argcomplete was installed in a virtual environment. Defaulting to user installation.", file=sys.stderr)
link_user_rcfiles()
elif args.user:
link_user_rcfiles()
else:
print("Defaulting to system-wide installation.", file=sys.stderr)
destinations.append(f"{get_zsh_system_dir()}/_python-argcomplete")
destinations.append(f"{get_bash_system_dir()}/python-argcomplete")
for destination in destinations:
install_to_destination(destination)
add_zsh_system_dir_to_fpath_for_user()
if args.dest is None:
print("Please restart your shell or source the installed file to activate it.", file=sys.stderr)
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,84 @@
#!/usr/bin/env python3
# Copyright 2012-2023, Andrey Kislyuk and argcomplete contributors.
# Licensed under the Apache License. See https://github.com/kislyuk/argcomplete for more info.
"""
This script is part of the Python argcomplete package (https://github.com/kislyuk/argcomplete).
It is used to check if an EASY-INSTALL-SCRIPT wrapper redirects to a script that contains the string
"PYTHON_ARGCOMPLETE_OK". If you have enabled global completion in argcomplete, the completion hook will run it every
time you press <TAB> in your shell.
Usage:
python-argcomplete-check-easy-install-script <input executable file>
"""
import sys
# PEP 366
__package__ = "argcomplete.scripts"
def main():
if len(sys.argv) != 2:
sys.exit(__doc__)
sys.tracebacklimit = 0
with open(sys.argv[1]) as fh:
line1, head = fh.read(1024).split("\n", 1)[:2]
if line1.startswith("#") and ("py" in line1 or "Py" in line1):
import re
lines = head.split("\n", 12)
for line in lines:
if line.startswith("# EASY-INSTALL-SCRIPT"):
import pkg_resources # type: ignore
re_match = re.match("# EASY-INSTALL-SCRIPT: '(.+)','(.+)'", line)
assert re_match is not None
dist, script = re_match.groups()
if "PYTHON_ARGCOMPLETE_OK" in pkg_resources.get_distribution(dist).get_metadata(
"scripts/" + script
):
return 0
elif line.startswith("# EASY-INSTALL-ENTRY-SCRIPT"):
re_match = re.match("# EASY-INSTALL-ENTRY-SCRIPT: '(.+)','(.+)','(.+)'", line)
assert re_match is not None
dist, group, name = re_match.groups()
import pkgutil
import pkg_resources # type: ignore
entry_point_info = pkg_resources.get_distribution(dist).get_entry_info(group, name)
assert entry_point_info is not None
module_name = entry_point_info.module_name
with open(pkgutil.get_loader(module_name).get_filename()) as mod_fh: # type: ignore
if "PYTHON_ARGCOMPLETE_OK" in mod_fh.read(1024):
return 0
elif line.startswith("# EASY-INSTALL-DEV-SCRIPT"):
for line2 in lines:
if line2.startswith("__file__"):
re_match = re.match("__file__ = '(.+)'", line2)
assert re_match is not None
filename = re_match.group(1)
with open(filename) as mod_fh:
if "PYTHON_ARGCOMPLETE_OK" in mod_fh.read(1024):
return 0
elif line.startswith("# PBR Generated"):
re_match = re.search("from (.*) import", head)
assert re_match is not None
module = re_match.groups()[0]
import pkgutil
import pkg_resources # type: ignore
with open(pkgutil.get_loader(module).get_filename()) as mod_fh: # type: ignore
if "PYTHON_ARGCOMPLETE_OK" in mod_fh.read(1024):
return 0
return 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,79 @@
#!/usr/bin/env python3
# PYTHON_ARGCOMPLETE_OK
# Copyright 2012-2023, Andrey Kislyuk and argcomplete contributors.
# Licensed under the Apache License. See https://github.com/kislyuk/argcomplete for more info.
"""
Register a Python executable for use with the argcomplete module.
To perform the registration, source the output of this script in your bash shell
(quote the output to avoid interpolation).
Example:
$ eval "$(register-python-argcomplete my-favorite-script.py)"
For Tcsh
$ eval `register-python-argcomplete --shell tcsh my-favorite-script.py`
For Fish
$ register-python-argcomplete --shell fish my-favourite-script.py > ~/.config/fish/my-favourite-script.py.fish
"""
import argparse
import sys
import argcomplete
# PEP 366
__package__ = "argcomplete.scripts"
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
"--no-defaults",
dest="use_defaults",
action="store_false",
default=True,
help="when no matches are generated, do not fallback to readline's default completion (affects bash only)",
)
parser.add_argument(
"--complete-arguments",
nargs=argparse.REMAINDER,
help="arguments to call complete with; use of this option discards default options (affects bash only)",
)
parser.add_argument(
"-s",
"--shell",
choices=("bash", "zsh", "tcsh", "fish", "powershell"),
default="bash",
help="output code for the specified shell",
)
parser.add_argument(
"-e", "--external-argcomplete-script", help="external argcomplete script for auto completion of the executable"
)
parser.add_argument("executable", nargs="+", help="executable to completed (when invoked by exactly this name)")
argcomplete.autocomplete(parser)
if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)
args = parser.parse_args()
sys.stdout.write(
argcomplete.shellcode(
args.executable, args.use_defaults, args.shell, args.complete_arguments, args.external_argcomplete_script
)
)
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,202 @@
# Copyright 2012-2023, Andrey Kislyuk and argcomplete contributors. Licensed under the terms of the
# `Apache License, Version 2.0 <http://www.apache.org/licenses/LICENSE-2.0>`_. Distribution of the LICENSE and NOTICE
# files with source copies of this package and derivative works is **REQUIRED** as specified by the Apache License.
# See https://github.com/kislyuk/argcomplete for more info.
from shlex import quote
bashcode = r"""#compdef %(executables)s
# Run something, muting output or redirecting it to the debug stream
# depending on the value of _ARC_DEBUG.
# If ARGCOMPLETE_USE_TEMPFILES is set, use tempfiles for IPC.
__python_argcomplete_run() {
if [[ -z "${ARGCOMPLETE_USE_TEMPFILES-}" ]]; then
__python_argcomplete_run_inner "$@"
return
fi
local tmpfile="$(mktemp)"
_ARGCOMPLETE_STDOUT_FILENAME="$tmpfile" __python_argcomplete_run_inner "$@"
local code=$?
cat "$tmpfile"
rm "$tmpfile"
return $code
}
__python_argcomplete_run_inner() {
if [[ -z "${_ARC_DEBUG-}" ]]; then
"$@" 8>&1 9>&2 1>/dev/null 2>&1 </dev/null
else
"$@" 8>&1 9>&2 1>&9 2>&1 </dev/null
fi
}
_python_argcomplete%(function_suffix)s() {
local IFS=$'\013'
local script="%(argcomplete_script)s"
if [[ -n "${ZSH_VERSION-}" ]]; then
local completions
completions=($(IFS="$IFS" \
COMP_LINE="$BUFFER" \
COMP_POINT="$CURSOR" \
_ARGCOMPLETE=1 \
_ARGCOMPLETE_SHELL="zsh" \
_ARGCOMPLETE_SUPPRESS_SPACE=1 \
__python_argcomplete_run ${script:-${words[1]}}))
local nosort=()
local nospace=()
if is-at-least 5.8; then
nosort=(-o nosort)
fi
if [[ "${completions-}" =~ ([^\\\\]): && "${match[1]}" =~ [=/:] ]]; then
nospace=(-S '')
fi
_describe "${words[1]}" completions "${nosort[@]}" "${nospace[@]}"
else
local SUPPRESS_SPACE=0
if compopt +o nospace 2> /dev/null; then
SUPPRESS_SPACE=1
fi
COMPREPLY=($(IFS="$IFS" \
COMP_LINE="$COMP_LINE" \
COMP_POINT="$COMP_POINT" \
COMP_TYPE="$COMP_TYPE" \
_ARGCOMPLETE_COMP_WORDBREAKS="$COMP_WORDBREAKS" \
_ARGCOMPLETE=1 \
_ARGCOMPLETE_SHELL="bash" \
_ARGCOMPLETE_SUPPRESS_SPACE=$SUPPRESS_SPACE \
__python_argcomplete_run ${script:-$1}))
if [[ $? != 0 ]]; then
unset COMPREPLY
elif [[ $SUPPRESS_SPACE == 1 ]] && [[ "${COMPREPLY-}" =~ [=/:]$ ]]; then
compopt -o nospace
fi
fi
}
if [[ -z "${ZSH_VERSION-}" ]]; then
complete %(complete_opts)s -F _python_argcomplete%(function_suffix)s %(executables)s
else
# When called by the Zsh completion system, this will end with
# "loadautofunc" when initially autoloaded and "shfunc" later on, otherwise,
# the script was "eval"-ed so use "compdef" to register it with the
# completion system
autoload is-at-least
if [[ $zsh_eval_context == *func ]]; then
_python_argcomplete%(function_suffix)s "$@"
else
compdef _python_argcomplete%(function_suffix)s %(executables)s
fi
fi
"""
tcshcode = """\
complete "%(executable)s" 'p@*@`python-argcomplete-tcsh "%(argcomplete_script)s"`@' ;
"""
fishcode = r"""
function __fish_%(function_name)s_complete
set -lx _ARGCOMPLETE 1
set -lx _ARGCOMPLETE_DFS \t
set -lx _ARGCOMPLETE_IFS \n
set -lx _ARGCOMPLETE_SUPPRESS_SPACE 1
set -lx _ARGCOMPLETE_SHELL fish
set -lx COMP_LINE (commandline -p)
set -lx COMP_POINT (string length (commandline -cp))
set -lx COMP_TYPE
if set -q _ARC_DEBUG
%(argcomplete_script)s 8>&1 9>&2 1>&9 2>&1
else
%(argcomplete_script)s 8>&1 9>&2 1>/dev/null 2>&1
end
end
complete %(completion_arg)s %(executable)s -f -a '(__fish_%(function_name)s_complete)'
"""
powershell_code = r"""
Register-ArgumentCompleter -Native -CommandName %(executable)s -ScriptBlock {
param($commandName, $wordToComplete, $cursorPosition)
$completion_file = New-TemporaryFile
$env:ARGCOMPLETE_USE_TEMPFILES = 1
$env:_ARGCOMPLETE_STDOUT_FILENAME = $completion_file
$env:COMP_LINE = $wordToComplete
$env:COMP_POINT = $cursorPosition
$env:_ARGCOMPLETE = 1
$env:_ARGCOMPLETE_SUPPRESS_SPACE = 0
$env:_ARGCOMPLETE_IFS = "`n"
$env:_ARGCOMPLETE_SHELL = "powershell"
%(argcomplete_script)s 2>&1 | Out-Null
Get-Content $completion_file | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)
}
Remove-Item $completion_file, Env:\_ARGCOMPLETE_STDOUT_FILENAME, Env:\ARGCOMPLETE_USE_TEMPFILES, Env:\COMP_LINE, Env:\COMP_POINT, Env:\_ARGCOMPLETE, Env:\_ARGCOMPLETE_SUPPRESS_SPACE, Env:\_ARGCOMPLETE_IFS, Env:\_ARGCOMPLETE_SHELL
}
""" # noqa: E501
shell_codes = {"bash": bashcode, "tcsh": tcshcode, "fish": fishcode, "powershell": powershell_code}
def shellcode(executables, use_defaults=True, shell="bash", complete_arguments=None, argcomplete_script=None):
"""
Provide the shell code required to register a python executable for use with the argcomplete module.
:param list(str) executables: Executables to be completed (when invoked exactly with this name)
:param bool use_defaults: Whether to fallback to readline's default completion when no matches are generated
(affects bash only)
:param str shell: Name of the shell to output code for
:param complete_arguments: Arguments to call complete with (affects bash only)
:type complete_arguments: list(str) or None
:param argcomplete_script: Script to call complete with, if not the executable to complete.
If supplied, will be used to complete *all* passed executables.
:type argcomplete_script: str or None
"""
if complete_arguments is None:
complete_options = "-o nospace -o default -o bashdefault" if use_defaults else "-o nospace -o bashdefault"
else:
complete_options = " ".join(complete_arguments)
if shell == "bash" or shell == "zsh":
quoted_executables = [quote(i) for i in executables]
executables_list = " ".join(quoted_executables)
script = argcomplete_script
if script:
# If the script path contain a space, this would generate an invalid function name.
function_suffix = "_" + script.replace(" ", "_SPACE_")
else:
script = ""
function_suffix = ""
code = bashcode % dict(
complete_opts=complete_options,
executables=executables_list,
argcomplete_script=script,
function_suffix=function_suffix,
)
elif shell == "fish":
code = ""
for executable in executables:
script = argcomplete_script or executable
completion_arg = "--path" if "/" in executable else "--command" # use path for absolute paths
function_name = executable.replace("/", "_") # / not allowed in function name
code += fishcode % dict(
executable=executable,
argcomplete_script=script,
completion_arg=completion_arg,
function_name=function_name,
)
elif shell == "powershell":
code = ""
for executable in executables:
script = argcomplete_script or executable
code += powershell_code % dict(executable=executable, argcomplete_script=script)
else:
code = ""
for executable in executables:
script = argcomplete_script
# If no script was specified, default to the executable being completed.
if not script:
script = executable
code += shell_codes.get(shell, "") % dict(executable=executable, argcomplete_script=script)
return code

View file

@ -0,0 +1 @@
pip

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,129 @@
../../Scripts/black.exe,sha256=lhcJd8I9AH1_FkLjfvqcnPOLHz0XFoqDhnLQB5N-h-0,106405
../../Scripts/blackd.exe,sha256=Jwo6pubjvwyPXYi3y9qnDeFYvZu9Bp2ydBcr5AXhSps,106406
30fcd23745efe32ce681__mypyc.cp310-win_amd64.pyd,sha256=pRx5IY_gCT0OEtnnUPYJ-S7KQFzZ18cVYpUNuZSV6RA,2723840
__pycache__/_black_version.cpython-310.pyc,,
_black_version.py,sha256=XVC2FRpYPCFN0Q1PSMLN8zuPbMVa0LuePuoHB8WIV64,20
_black_version.pyi,sha256=gXF2iFKaJ_mJncCEFLjOHdt3GxGdW_dheaJIpcZ2IwA,14
black-26.3.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
black-26.3.1.dist-info/METADATA,sha256=-mxo0oe8mMLTKryPQmYKZ3kNYIIzqE0ZfKRALpwfd4Y,91891
black-26.3.1.dist-info/RECORD,,
black-26.3.1.dist-info/WHEEL,sha256=IEheya3TgCujf4RsksFGJu9E_Hw3yzEacSYrp-thffM,97
black-26.3.1.dist-info/entry_points.txt,sha256=XTCA4X2yVA0tMiV7l96Gv9TyxhVhoCaznLN2XThqYSA,144
black-26.3.1.dist-info/licenses/LICENSE,sha256=XQJSBb4crFXeCOvZ-WHsfXTQ-Zj2XxeFbd0ien078zM,1101
black/__init__.cp310-win_amd64.pyd,sha256=L54lM-4wF3DHk-p9O4CHFjDJlL5krNRM5RpeSJBbR6Q,10752
black/__init__.py,sha256=lhUdQrZYaGHJJCkbLo-kpT24OWakCxY_aZpgoe3FCzo,56459
black/__main__.py,sha256=6V0pV9Zeh8940mbQbVTCPdTX4Gjq1HGrFCA6E4HLGaM,50
black/__pycache__/__init__.cpython-310.pyc,,
black/__pycache__/__main__.cpython-310.pyc,,
black/__pycache__/_width_table.cpython-310.pyc,,
black/__pycache__/brackets.cpython-310.pyc,,
black/__pycache__/cache.cpython-310.pyc,,
black/__pycache__/comments.cpython-310.pyc,,
black/__pycache__/concurrency.cpython-310.pyc,,
black/__pycache__/const.cpython-310.pyc,,
black/__pycache__/debug.cpython-310.pyc,,
black/__pycache__/files.cpython-310.pyc,,
black/__pycache__/handle_ipynb_magics.cpython-310.pyc,,
black/__pycache__/linegen.cpython-310.pyc,,
black/__pycache__/lines.cpython-310.pyc,,
black/__pycache__/mode.cpython-310.pyc,,
black/__pycache__/nodes.cpython-310.pyc,,
black/__pycache__/numerics.cpython-310.pyc,,
black/__pycache__/output.cpython-310.pyc,,
black/__pycache__/parsing.cpython-310.pyc,,
black/__pycache__/ranges.cpython-310.pyc,,
black/__pycache__/report.cpython-310.pyc,,
black/__pycache__/rusty.cpython-310.pyc,,
black/__pycache__/schema.cpython-310.pyc,,
black/__pycache__/strings.cpython-310.pyc,,
black/__pycache__/trans.cpython-310.pyc,,
black/_width_table.cp310-win_amd64.pyd,sha256=3-ePVeUx_8MmrbCNPsL7ApaULMWxPMq4XoeaRjvpypg,10752
black/_width_table.py,sha256=nOVdpn2H49kF-h4bPzNAJ_zfb6PJ7a1czEx4N124hq0,3228
black/brackets.cp310-win_amd64.pyd,sha256=daWUB_RaD40HspuiWWbK58hjXuK1hHwged6NKWdPgyQ,10752
black/brackets.py,sha256=ugO-Aga8aidqXdgqwlHNcBNBcxV1QtEWVPsT5JwXelY,12793
black/cache.cp310-win_amd64.pyd,sha256=6EaoXprryOVxGp_M0iBwjsUu9iSNIfTEDtATRaB3ks8,10752
black/cache.py,sha256=ty9qn9qL7dz7a82dFa8zYFvQprEL4avnJ6zAlDcqwqA,4904
black/comments.cp310-win_amd64.pyd,sha256=sYjdbgOUbHjeslo3Poqf5t6Jb_NSkf5rkADjFhoINq4,10752
black/comments.py,sha256=pRHiP7N6ZxkmtW2hXlFiRUErMw566j188kSrtdMH194,30580
black/concurrency.py,sha256=_b74I3Nwo3Dh4OYOW8mLZdATvQgYZnVGSulZPY1MTAg,7794
black/const.cp310-win_amd64.pyd,sha256=f1QmZs7xbbQqYISzBJYF_zCe60U4rTCa9nQeS3GOsfc,10752
black/const.py,sha256=FP5YcSxH6Cb0jqSkwF0nI4dHxPyQtL34hoWBfAqnAhI,325
black/debug.py,sha256=DJ5Cm2HaJT_J8H5_hqw-gYX1nVX5rh8595e2xagmRKI,1977
black/files.py,sha256=-wVRP-4ZOUM8x3ylqiESy6QXa2pTpv_e63b2Sh0w2t0,15090
black/handle_ipynb_magics.cp310-win_amd64.pyd,sha256=G91PG1b_ti09i66GKK6cQNd-SVIxoZnDI6BK2-VsqbM,10752
black/handle_ipynb_magics.py,sha256=4dDae_maXzIXdjK6lwVBi6hzRFzrCG9Bht7IsYy6xdU,16416
black/linegen.cp310-win_amd64.pyd,sha256=fT5E-jPH-o-_JdwGt_6n7HX1vgbTub7MEQ55gzWCK28,10752
black/linegen.py,sha256=gWWdrjPRFqattB2lB3cjq13k6-4LTo9RxnOb4b5EplI,80963
black/lines.cp310-win_amd64.pyd,sha256=rmapn6CaW6h5SWM_XRbo2DOj27mIyAi41SJxV08_fgE,10752
black/lines.py,sha256=VU467q5lSrOGhrz5aBA7lupTVHHrln-P8LUtZf1w25M,43008
black/mode.cp310-win_amd64.pyd,sha256=ceKQLApCljoRr5kGSn7-qkG4a8EtFzPpf-3A8clXHaA,10752
black/mode.py,sha256=T8cW29hIPF4LfcpylQsMrI4mrBq6onpOlhDInPvY3Fk,10563
black/nodes.cp310-win_amd64.pyd,sha256=gPJdyZnN7cnHHzm9noaymedyWUOsCSQJ_CMj955TYL4,10752
black/nodes.py,sha256=kz60pL_XrHXfKSSB7tAp1IfKtSNRqzJmW8QufRjeMmM,33285
black/numerics.cp310-win_amd64.pyd,sha256=xtQTv2Ev8lv2v7Y76IXjWnTle9-LmzZfrWo66jrf4z0,10752
black/numerics.py,sha256=gB1T1-npxj44Vhex63ov-oGsoIPwx_PZlT-n_qUcwO4,1716
black/output.py,sha256=4hEQsqNDlgWZTAJ-n0muusxin2HR8AxSNRmuZrjWoCM,4033
black/parsing.cp310-win_amd64.pyd,sha256=_HGIpSgugg28qylaF1J7yfylzQhvh30R0_-Ygy7B2NU,10752
black/parsing.py,sha256=eJB0i3dCwNQe2xjBqwsltH81bZMNagPax-6hIceRuF0,8671
black/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
black/ranges.cp310-win_amd64.pyd,sha256=ASUtNiCp0uxjr1-6y602Ai5DE4hwcA7Jg2bDAzxZq4o,10752
black/ranges.py,sha256=V6JhiIQCNUhgykRTsgdhqwz0CzHu3VeYk2JuRk5lxRw,21128
black/report.py,sha256=8Xies3PseQeTN4gYfHS7RewVQRjDsDBfFDR3sSNytco,3559
black/resources/__init__.cp310-win_amd64.pyd,sha256=ARg5-6QvY_72FGxJZKZCqLjly9cOzGvHqaFMc3dsaXk,10752
black/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
black/resources/__pycache__/__init__.cpython-310.pyc,,
black/resources/black.schema.json,sha256=qnDD2eskOv808T9d-NLuJNnf-wPIJJA46xI0Crd7E9I,7539
black/rusty.cp310-win_amd64.pyd,sha256=ISpVXhA4ts_TJeGICydabM4PGOFYZbx1Zb04v1knlZY,10752
black/rusty.py,sha256=RogIomJ1RCLMTOK_RA6U3EMbzWV_ZHxPtrXveXbMjzQ,585
black/schema.cp310-win_amd64.pyd,sha256=3iGsU3DJvIFNaJO4GX8TwC-pGq_On9Py33c0r0k0_GM,10752
black/schema.py,sha256=ZLKjanGVK4bG12GD7mkDzjOtLkv_g86p1-TXrdN9Skc,446
black/strings.cp310-win_amd64.pyd,sha256=W0-3Bp5rqsoW91NpdCHSeAnuVciXYaa1VBdMNDLi_sA,10752
black/strings.py,sha256=YOkxFUbnc228R2gQjMTA-QTAko9WDuOFykddBnEsYNI,13695
black/trans.cp310-win_amd64.pyd,sha256=k3xhkx-ZlSljtR0sF5IdpcochmDZ3Vg6XOwxGhy5_oM,10752
black/trans.py,sha256=svyMSYybDMDSYKM-EHYHPwNOsKpINsmd6mClZvVAdQA,99638
blackd/__init__.py,sha256=3Ethlw9pAXQVzT7eGf2MnvTzjjonF1t9d-FiTi7rwGU,10664
blackd/__main__.py,sha256=-2NrSIZ5Es7pTFThp8w5JL9LwmmxtF1akhe7NU1OGvs,40
blackd/__pycache__/__init__.cpython-310.pyc,,
blackd/__pycache__/__main__.cpython-310.pyc,,
blackd/__pycache__/client.cpython-310.pyc,,
blackd/__pycache__/middlewares.cpython-310.pyc,,
blackd/client.py,sha256=u7mkAPVsONR2Y4ucQNFgCLwK3WOHX7UojGQzXKpJDCI,4078
blackd/middlewares.py,sha256=mnIQU5_0sp7sVW5RESY9fSNxBDr9wpRsXdNA1_bYAIQ,1532
blib2to3/Grammar.txt,sha256=sYVk_h721g2t_Y_VKFjCgqCdBEDbVkA4aGkOPyFVRug,12290
blib2to3/LICENSE,sha256=D2HM6JsydKABNqFe2-_N4Lf8VxxE1_5DVQtAFzw2_w8,13016
blib2to3/PatternGrammar.txt,sha256=tH-u-ZLZsHJGgjZEfXN2S8GVCKXleQwWvOf7sO4hlmo,835
blib2to3/README,sha256=G-DiXkC8aKINCNv7smI2q_mz-8k6kC4yYO2OrMb0Nqs,1098
blib2to3/__init__.py,sha256=CSR2VOIKJL-JnGG41PcfbQZQEPCw43jfeK_EUisNsFQ,9
blib2to3/__pycache__/__init__.cpython-310.pyc,,
blib2to3/__pycache__/pygram.cpython-310.pyc,,
blib2to3/__pycache__/pytree.cpython-310.pyc,,
blib2to3/pgen2/__init__.py,sha256=z8NemtNtAaIBocPMl0aMLgxaQMedsKOS_dOVAy8c3TI,147
blib2to3/pgen2/__pycache__/__init__.cpython-310.pyc,,
blib2to3/pgen2/__pycache__/conv.cpython-310.pyc,,
blib2to3/pgen2/__pycache__/driver.cpython-310.pyc,,
blib2to3/pgen2/__pycache__/grammar.cpython-310.pyc,,
blib2to3/pgen2/__pycache__/literals.cpython-310.pyc,,
blib2to3/pgen2/__pycache__/parse.cpython-310.pyc,,
blib2to3/pgen2/__pycache__/pgen.cpython-310.pyc,,
blib2to3/pgen2/__pycache__/token.cpython-310.pyc,,
blib2to3/pgen2/__pycache__/tokenize.cpython-310.pyc,,
blib2to3/pgen2/conv.cp310-win_amd64.pyd,sha256=mC_dVmr1ePRMsx80Aux7HvPEZFr7bVeEdSKpgTgec_0,10752
blib2to3/pgen2/conv.py,sha256=oxdquGP469PeQwFmGM92A9FJEtZlQVb7_v4nFqzzsXY,9852
blib2to3/pgen2/driver.cp310-win_amd64.pyd,sha256=tfFfDPKpBhAef2uWB06m8zXhXVLdmWD3D_WR7pqBDiI,10752
blib2to3/pgen2/driver.py,sha256=MSY0gFbSqNfvdjD81gLFK5-sTIebRZqPRs90-ZLE47Q,10756
blib2to3/pgen2/grammar.cp310-win_amd64.pyd,sha256=iEJuT9EGThsxg0RM183_GzHcN2tgN9bzF-JpP5CCrNk,10752
blib2to3/pgen2/grammar.py,sha256=kWLJf3bdvHO4g_b3M_EEo98YIoBAwUBtFFYWd7kFH0c,7074
blib2to3/pgen2/literals.cp310-win_amd64.pyd,sha256=GZjTMJNnTbZ9--Ys0cYI7GiFHnCPaI7o9k4EKyle1nM,10752
blib2to3/pgen2/literals.py,sha256=eWQ54eEpsIJ5wohd__6dd0kk960QM5eCgMSgkg7eFZk,1645
blib2to3/pgen2/parse.cp310-win_amd64.pyd,sha256=QBJGwKFsC1TAIjZW_S2hU7m3c1HS6vAbq2kONT_-MGg,10752
blib2to3/pgen2/parse.py,sha256=RaVLB5PKkVv-OWQR02hOhfVPzUXr_wp8vKyTYjVTf08,15875
blib2to3/pgen2/pgen.cp310-win_amd64.pyd,sha256=mxPD0jlnzHjnQuUCCDvSnieIQ9gMUOfxp6n5gE1t5_M,10752
blib2to3/pgen2/pgen.py,sha256=wECI5dJFX4j_4HNL0s1V4YnvbdgKN-c56dToxAoCj3g,15489
blib2to3/pgen2/token.cp310-win_amd64.pyd,sha256=hPL9mktghHVQn5H_woMXV5ZJhpYvAJN0TGVjurakZ9g,10752
blib2to3/pgen2/token.py,sha256=zm2Ah0psZm5khl8fFQKwfKq6QQW_nlfEJjDHfJoXPkU,2065
blib2to3/pgen2/tokenize.cp310-win_amd64.pyd,sha256=jk8G3Cy4lw3pbh9fMjOucvBhO5dT74z6G4DrYd1i95c,10752
blib2to3/pgen2/tokenize.py,sha256=PFQYuR-Wy2G_fthIjGIFexDOCzVvkBP0yXk19zC7I1A,7604
blib2to3/pygram.cp310-win_amd64.pyd,sha256=lcaj-c3XjqwA53XGhRYvh5EUyECZTFGvEfj-Am7ap8I,10752
blib2to3/pygram.py,sha256=lZcZqHOonqdsUed8s83obK61RKkDoJVXmhdn9tBQJqs,5228
blib2to3/pytree.cp310-win_amd64.pyd,sha256=CpdO2hMynemn0cS1bSnIu-_jlwMdy2MnF0Fkl0uKb4U,10752
blib2to3/pytree.py,sha256=QG4GKi89fI18tHjfGm_Lf9v6Gh0PlVIR4PdS8ZfE47Q,33387

View file

@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: hatchling 1.29.0
Root-Is-Purelib: false
Tag: cp310-cp310-win_amd64

View file

@ -0,0 +1,6 @@
[console_scripts]
black = black:patched_main
blackd = blackd:patched_main [d]
[validate_pyproject.tool_schema]
black = black.schema:get_schema

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Łukasz Langa
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
from black import patched_main
patched_main()

View file

@ -0,0 +1,132 @@
# Generated by make_width_table.py
# wcwidth 0.2.14
# Unicode 17.0.0
from typing import Final
WIDTH_TABLE: Final[list[tuple[int, int, int]]] = [
(4352, 4447, 2),
(8986, 8987, 2),
(9001, 9002, 2),
(9193, 9196, 2),
(9200, 9200, 2),
(9203, 9203, 2),
(9725, 9726, 2),
(9748, 9749, 2),
(9776, 9783, 2),
(9800, 9811, 2),
(9855, 9855, 2),
(9866, 9871, 2),
(9875, 9875, 2),
(9889, 9889, 2),
(9898, 9899, 2),
(9917, 9918, 2),
(9924, 9925, 2),
(9934, 9934, 2),
(9940, 9940, 2),
(9962, 9962, 2),
(9970, 9971, 2),
(9973, 9973, 2),
(9978, 9978, 2),
(9981, 9981, 2),
(9989, 9989, 2),
(9994, 9995, 2),
(10024, 10024, 2),
(10060, 10060, 2),
(10062, 10062, 2),
(10067, 10069, 2),
(10071, 10071, 2),
(10133, 10135, 2),
(10160, 10160, 2),
(10175, 10175, 2),
(11035, 11036, 2),
(11088, 11088, 2),
(11093, 11093, 2),
(11904, 11929, 2),
(11931, 12019, 2),
(12032, 12245, 2),
(12272, 12329, 2),
(12336, 12350, 2),
(12353, 12438, 2),
(12443, 12543, 2),
(12549, 12591, 2),
(12593, 12686, 2),
(12688, 12773, 2),
(12783, 12830, 2),
(12832, 12871, 2),
(12880, 42124, 2),
(42128, 42182, 2),
(43360, 43388, 2),
(44032, 55203, 2),
(63744, 64255, 2),
(65040, 65049, 2),
(65072, 65106, 2),
(65108, 65126, 2),
(65128, 65131, 2),
(65281, 65376, 2),
(65504, 65510, 2),
(94176, 94179, 2),
(94194, 94198, 2),
(94208, 101589, 2),
(101631, 101662, 2),
(101760, 101874, 2),
(110576, 110579, 2),
(110581, 110587, 2),
(110589, 110590, 2),
(110592, 110882, 2),
(110898, 110898, 2),
(110928, 110930, 2),
(110933, 110933, 2),
(110948, 110951, 2),
(110960, 111355, 2),
(119552, 119638, 2),
(119648, 119670, 2),
(126980, 126980, 2),
(127183, 127183, 2),
(127374, 127374, 2),
(127377, 127386, 2),
(127488, 127490, 2),
(127504, 127547, 2),
(127552, 127560, 2),
(127568, 127569, 2),
(127584, 127589, 2),
(127744, 127776, 2),
(127789, 127797, 2),
(127799, 127868, 2),
(127870, 127891, 2),
(127904, 127946, 2),
(127951, 127955, 2),
(127968, 127984, 2),
(127988, 127988, 2),
(127992, 127994, 2),
(128000, 128062, 2),
(128064, 128064, 2),
(128066, 128252, 2),
(128255, 128317, 2),
(128331, 128334, 2),
(128336, 128359, 2),
(128378, 128378, 2),
(128405, 128406, 2),
(128420, 128420, 2),
(128507, 128591, 2),
(128640, 128709, 2),
(128716, 128716, 2),
(128720, 128722, 2),
(128725, 128728, 2),
(128732, 128735, 2),
(128747, 128748, 2),
(128756, 128764, 2),
(128992, 129003, 2),
(129008, 129008, 2),
(129292, 129338, 2),
(129340, 129349, 2),
(129351, 129535, 2),
(129648, 129660, 2),
(129664, 129674, 2),
(129678, 129734, 2),
(129736, 129736, 2),
(129741, 129756, 2),
(129759, 129770, 2),
(129775, 129784, 2),
(131072, 196605, 2),
(196608, 262141, 2),
]

View file

@ -0,0 +1,383 @@
"""Builds on top of nodes.py to track brackets."""
from collections.abc import Iterable, Sequence
from dataclasses import dataclass, field
from typing import Final, Union
from black.nodes import (
BRACKET,
CLOSING_BRACKETS,
COMPARATORS,
LOGIC_OPERATORS,
MATH_OPERATORS,
OPENING_BRACKETS,
UNPACKING_PARENTS,
VARARGS_PARENTS,
is_vararg,
syms,
)
from blib2to3.pgen2 import token
from blib2to3.pytree import Leaf, Node
# types
LN = Union[Leaf, Node]
Depth = int
LeafID = int
NodeType = int
Priority = int
COMPREHENSION_PRIORITY: Final = 20
COMMA_PRIORITY: Final = 18
TERNARY_PRIORITY: Final = 16
LOGIC_PRIORITY: Final = 14
STRING_PRIORITY: Final = 12
COMPARATOR_PRIORITY: Final = 10
MATH_PRIORITIES: Final = {
token.VBAR: 9,
token.CIRCUMFLEX: 8,
token.AMPER: 7,
token.LEFTSHIFT: 6,
token.RIGHTSHIFT: 6,
token.PLUS: 5,
token.MINUS: 5,
token.STAR: 4,
token.SLASH: 4,
token.DOUBLESLASH: 4,
token.PERCENT: 4,
token.AT: 4,
token.TILDE: 3,
token.DOUBLESTAR: 2,
}
DOT_PRIORITY: Final = 1
class BracketMatchError(Exception):
"""Raised when an opening bracket is unable to be matched to a closing bracket."""
@dataclass
class BracketTracker:
"""Keeps track of brackets on a line."""
depth: int = 0
bracket_match: dict[tuple[Depth, NodeType], Leaf] = field(default_factory=dict)
delimiters: dict[LeafID, Priority] = field(default_factory=dict)
previous: Leaf | None = None
_for_loop_depths: list[int] = field(default_factory=list)
_lambda_argument_depths: list[int] = field(default_factory=list)
invisible: list[Leaf] = field(default_factory=list)
def mark(self, leaf: Leaf) -> None:
"""Mark `leaf` with bracket-related metadata. Keep track of delimiters.
All leaves receive an int `bracket_depth` field that stores how deep
within brackets a given leaf is. 0 means there are no enclosing brackets
that started on this line.
If a leaf is itself a closing bracket and there is a matching opening
bracket earlier, it receives an `opening_bracket` field with which it forms a
pair. This is a one-directional link to avoid reference cycles. Closing
bracket without opening happens on lines continued from previous
breaks, e.g. `) -> "ReturnType":` as part of a funcdef where we place
the return type annotation on its own line of the previous closing RPAR.
If a leaf is a delimiter (a token on which Black can split the line if
needed) and it's on depth 0, its `id()` is stored in the tracker's
`delimiters` field.
"""
if leaf.type == token.COMMENT:
return
if (
self.depth == 0
and leaf.type in CLOSING_BRACKETS
and (self.depth, leaf.type) not in self.bracket_match
):
return
self.maybe_decrement_after_for_loop_variable(leaf)
self.maybe_decrement_after_lambda_arguments(leaf)
if leaf.type in CLOSING_BRACKETS:
self.depth -= 1
try:
opening_bracket = self.bracket_match.pop((self.depth, leaf.type))
except KeyError as e:
raise BracketMatchError(
"Unable to match a closing bracket to the following opening"
f" bracket: {leaf}"
) from e
leaf.opening_bracket = opening_bracket
if not leaf.value:
self.invisible.append(leaf)
leaf.bracket_depth = self.depth
if self.depth == 0:
delim = is_split_before_delimiter(leaf, self.previous)
if delim and self.previous is not None:
self.delimiters[id(self.previous)] = delim
else:
delim = is_split_after_delimiter(leaf)
if delim:
self.delimiters[id(leaf)] = delim
if leaf.type in OPENING_BRACKETS:
self.bracket_match[self.depth, BRACKET[leaf.type]] = leaf
self.depth += 1
if not leaf.value:
self.invisible.append(leaf)
self.previous = leaf
self.maybe_increment_lambda_arguments(leaf)
self.maybe_increment_for_loop_variable(leaf)
def any_open_for_or_lambda(self) -> bool:
"""Return True if there is an open for or lambda expression on the line.
See maybe_increment_for_loop_variable and maybe_increment_lambda_arguments
for details."""
return bool(self._for_loop_depths or self._lambda_argument_depths)
def any_open_brackets(self) -> bool:
"""Return True if there is an yet unmatched open bracket on the line."""
return bool(self.bracket_match)
def max_delimiter_priority(self, exclude: Iterable[LeafID] = ()) -> Priority:
"""Return the highest priority of a delimiter found on the line.
Values are consistent with what `is_split_*_delimiter()` return.
Raises ValueError on no delimiters.
"""
return max(v for k, v in self.delimiters.items() if k not in exclude)
def delimiter_count_with_priority(self, priority: Priority = 0) -> int:
"""Return the number of delimiters with the given `priority`.
If no `priority` is passed, defaults to max priority on the line.
"""
if not self.delimiters:
return 0
priority = priority or self.max_delimiter_priority()
return sum(1 for p in self.delimiters.values() if p == priority)
def maybe_increment_for_loop_variable(self, leaf: Leaf) -> bool:
"""In a for loop, or comprehension, the variables are often unpacks.
To avoid splitting on the comma in this situation, increase the depth of
tokens between `for` and `in`.
"""
if leaf.type == token.NAME and leaf.value == "for":
self.depth += 1
self._for_loop_depths.append(self.depth)
return True
return False
def maybe_decrement_after_for_loop_variable(self, leaf: Leaf) -> bool:
"""See `maybe_increment_for_loop_variable` above for explanation."""
if (
self._for_loop_depths
and self._for_loop_depths[-1] == self.depth
and leaf.type == token.NAME
and leaf.value == "in"
):
self.depth -= 1
self._for_loop_depths.pop()
return True
return False
def maybe_increment_lambda_arguments(self, leaf: Leaf) -> bool:
"""In a lambda expression, there might be more than one argument.
To avoid splitting on the comma in this situation, increase the depth of
tokens between `lambda` and `:`.
"""
if leaf.type == token.NAME and leaf.value == "lambda":
self.depth += 1
self._lambda_argument_depths.append(self.depth)
return True
return False
def maybe_decrement_after_lambda_arguments(self, leaf: Leaf) -> bool:
"""See `maybe_increment_lambda_arguments` above for explanation."""
if (
self._lambda_argument_depths
and self._lambda_argument_depths[-1] == self.depth
and leaf.type == token.COLON
):
self.depth -= 1
self._lambda_argument_depths.pop()
return True
return False
def get_open_lsqb(self) -> Leaf | None:
"""Return the most recent opening square bracket (if any)."""
return self.bracket_match.get((self.depth - 1, token.RSQB))
def is_split_after_delimiter(leaf: Leaf) -> Priority:
"""Return the priority of the `leaf` delimiter, given a line break after it.
The delimiter priorities returned here are from those delimiters that would
cause a line break after themselves.
Higher numbers are higher priority.
"""
if leaf.type == token.COMMA:
return COMMA_PRIORITY
return 0
def is_split_before_delimiter(leaf: Leaf, previous: Leaf | None = None) -> Priority:
"""Return the priority of the `leaf` delimiter, given a line break before it.
The delimiter priorities returned here are from those delimiters that would
cause a line break before themselves.
Higher numbers are higher priority.
"""
if is_vararg(leaf, within=VARARGS_PARENTS | UNPACKING_PARENTS):
# * and ** might also be MATH_OPERATORS but in this case they are not.
# Don't treat them as a delimiter.
return 0
if (
leaf.type == token.DOT
and leaf.parent
and leaf.parent.type not in {syms.import_from, syms.dotted_name}
and (previous is None or previous.type in CLOSING_BRACKETS)
):
return DOT_PRIORITY
if (
leaf.type in MATH_OPERATORS
and leaf.parent
and leaf.parent.type not in {syms.factor, syms.star_expr}
):
return MATH_PRIORITIES[leaf.type]
if leaf.type in COMPARATORS:
return COMPARATOR_PRIORITY
if (
leaf.type == token.STRING
and previous is not None
and previous.type == token.STRING
):
return STRING_PRIORITY
if leaf.type not in {token.NAME, token.ASYNC}:
return 0
if (
leaf.value == "for"
and leaf.parent
and leaf.parent.type in {syms.comp_for, syms.old_comp_for}
or leaf.type == token.ASYNC
):
if (
not isinstance(leaf.prev_sibling, Leaf)
or leaf.prev_sibling.value != "async"
):
return COMPREHENSION_PRIORITY
if (
leaf.value == "if"
and leaf.parent
and leaf.parent.type in {syms.comp_if, syms.old_comp_if}
):
return COMPREHENSION_PRIORITY
if leaf.value in {"if", "else"} and leaf.parent and leaf.parent.type == syms.test:
return TERNARY_PRIORITY
if leaf.value == "is":
return COMPARATOR_PRIORITY
if (
leaf.value == "in"
and leaf.parent
and leaf.parent.type in {syms.comp_op, syms.comparison}
and not (
previous is not None
and previous.type == token.NAME
and previous.value == "not"
)
):
return COMPARATOR_PRIORITY
if (
leaf.value == "not"
and leaf.parent
and leaf.parent.type == syms.comp_op
and not (
previous is not None
and previous.type == token.NAME
and previous.value == "is"
)
):
return COMPARATOR_PRIORITY
if leaf.value in LOGIC_OPERATORS and leaf.parent:
return LOGIC_PRIORITY
return 0
def max_delimiter_priority_in_atom(node: LN) -> Priority:
"""Return maximum delimiter priority inside `node`.
This is specific to atoms with contents contained in a pair of parentheses.
If `node` isn't an atom or there are no enclosing parentheses, returns 0.
"""
if node.type != syms.atom:
return 0
first = node.children[0]
last = node.children[-1]
if not (first.type == token.LPAR and last.type == token.RPAR):
return 0
bt = BracketTracker()
for c in node.children[1:-1]:
if isinstance(c, Leaf):
bt.mark(c)
else:
for leaf in c.leaves():
bt.mark(leaf)
try:
return bt.max_delimiter_priority()
except ValueError:
return 0
def get_leaves_inside_matching_brackets(leaves: Sequence[Leaf]) -> set[LeafID]:
"""Return leaves that are inside matching brackets.
The input `leaves` can have non-matching brackets at the head or tail parts.
Matching brackets are included.
"""
try:
# Start with the first opening bracket and ignore closing brackets before.
start_index = next(
i for i, l in enumerate(leaves) if l.type in OPENING_BRACKETS
)
except StopIteration:
return set()
bracket_stack = []
ids = set()
for i in range(start_index, len(leaves)):
leaf = leaves[i]
if leaf.type in OPENING_BRACKETS:
bracket_stack.append((BRACKET[leaf.type], i))
if leaf.type in CLOSING_BRACKETS:
if bracket_stack and leaf.type == bracket_stack[-1][0]:
_, start = bracket_stack.pop()
for j in range(start, i + 1):
ids.add(id(leaves[j]))
else:
break
return ids

View file

@ -0,0 +1,150 @@
"""Caching of formatted files with feature-based invalidation."""
import hashlib
import os
import pickle
import sys
import tempfile
from collections.abc import Iterable
from dataclasses import dataclass, field
from pathlib import Path
from typing import NamedTuple
from platformdirs import user_cache_dir
from _black_version import version as __version__
from black.mode import Mode
from black.output import err
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self
class FileData(NamedTuple):
st_mtime: float
st_size: int
hash: str
def get_cache_dir() -> Path:
"""Get the cache directory used by black.
Users can customize this directory on all systems using `BLACK_CACHE_DIR`
environment variable. By default, the cache directory is the user cache directory
under the black application.
This result is immediately set to a constant `black.cache.CACHE_DIR` as to avoid
repeated calls.
"""
# NOTE: Function mostly exists as a clean way to test getting the cache directory.
default_cache_dir = user_cache_dir("black")
cache_dir = Path(os.environ.get("BLACK_CACHE_DIR", default_cache_dir))
cache_dir = cache_dir / __version__
return cache_dir
CACHE_DIR = get_cache_dir()
def get_cache_file(mode: Mode) -> Path:
return CACHE_DIR / f"cache.{mode.get_cache_key()}.pickle"
@dataclass
class Cache:
mode: Mode
cache_file: Path
file_data: dict[str, FileData] = field(default_factory=dict)
@classmethod
def read(cls, mode: Mode) -> Self:
"""Read the cache if it exists and is well-formed.
If it is not well-formed, the call to write later should
resolve the issue.
"""
cache_file = get_cache_file(mode)
try:
exists = cache_file.exists()
except OSError as e:
# Likely file too long; see #4172 and #4174
err(f"Unable to read cache file {cache_file} due to {e}")
return cls(mode, cache_file)
if not exists:
return cls(mode, cache_file)
with cache_file.open("rb") as fobj:
try:
data: dict[str, tuple[float, int, str]] = pickle.load(fobj)
file_data = {k: FileData(*v) for k, v in data.items()}
except (pickle.UnpicklingError, ValueError, IndexError):
return cls(mode, cache_file)
return cls(mode, cache_file, file_data)
@staticmethod
def hash_digest(path: Path) -> str:
"""Return hash digest for path."""
data = path.read_bytes()
return hashlib.sha256(data).hexdigest()
@staticmethod
def get_file_data(path: Path) -> FileData:
"""Return file data for path."""
stat = path.stat()
hash = Cache.hash_digest(path)
return FileData(stat.st_mtime, stat.st_size, hash)
def is_changed(self, source: Path) -> bool:
"""Check if source has changed compared to cached version."""
res_src = source.resolve()
old = self.file_data.get(str(res_src))
if old is None:
return True
st = res_src.stat()
if st.st_size != old.st_size:
return True
if st.st_mtime != old.st_mtime:
new_hash = Cache.hash_digest(res_src)
if new_hash != old.hash:
return True
return False
def filtered_cached(self, sources: Iterable[Path]) -> tuple[set[Path], set[Path]]:
"""Split an iterable of paths in `sources` into two sets.
The first contains paths of files that modified on disk or are not in the
cache. The other contains paths to non-modified files.
"""
changed: set[Path] = set()
done: set[Path] = set()
for src in sources:
if self.is_changed(src):
changed.add(src)
else:
done.add(src)
return changed, done
def write(self, sources: Iterable[Path]) -> None:
"""Update the cache file data and write a new cache file."""
self.file_data.update(
**{str(src.resolve()): Cache.get_file_data(src) for src in sources}
)
try:
CACHE_DIR.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(
dir=str(self.cache_file.parent), delete=False
) as f:
# We store raw tuples in the cache because it's faster.
data: dict[str, tuple[float, int, str]] = {
k: (*v,) for k, v in self.file_data.items()
}
pickle.dump(data, f, protocol=4)
os.replace(f.name, self.cache_file)
except OSError:
pass

View file

@ -0,0 +1,827 @@
import re
from collections.abc import Collection, Iterator
from dataclasses import dataclass
from functools import lru_cache
from typing import Final, Union
from black.mode import Mode
from black.nodes import (
CLOSING_BRACKETS,
STANDALONE_COMMENT,
STATEMENT,
WHITESPACE,
container_of,
first_leaf_of,
is_type_comment_string,
make_simple_prefix,
preceding_leaf,
syms,
)
from blib2to3.pgen2 import token
from blib2to3.pytree import Leaf, Node
# types
LN = Union[Leaf, Node]
FMT_OFF: Final = {"# fmt: off", "# fmt:off", "# yapf: disable"}
FMT_SKIP: Final = {"# fmt: skip", "# fmt:skip"}
FMT_ON: Final = {"# fmt: on", "# fmt:on", "# yapf: enable"}
# Compound statements we care about for fmt: skip handling
# (excludes except_clause and case_block which aren't standalone compound statements)
_COMPOUND_STATEMENTS: Final = STATEMENT - {syms.except_clause, syms.case_block}
COMMENT_EXCEPTIONS = " !:#'"
_COMMENT_PREFIX = "# "
_COMMENT_LIST_SEPARATOR = ";"
@dataclass
class ProtoComment:
"""Describes a piece of syntax that is a comment.
It's not a :class:`blib2to3.pytree.Leaf` so that:
* it can be cached (`Leaf` objects should not be reused more than once as
they store their lineno, column, prefix, and parent information);
* `newlines` and `consumed` fields are kept separate from the `value`. This
simplifies handling of special marker comments like ``# fmt: off/on``.
"""
type: int # token.COMMENT or STANDALONE_COMMENT
value: str # content of the comment
newlines: int # how many newlines before the comment
consumed: int # how many characters of the original leaf's prefix did we consume
form_feed: bool # is there a form feed before the comment
leading_whitespace: str # leading whitespace before the comment, if any
def generate_comments(leaf: LN, mode: Mode) -> Iterator[Leaf]:
"""Clean the prefix of the `leaf` and generate comments from it, if any.
Comments in lib2to3 are shoved into the whitespace prefix. This happens
in `pgen2/driver.py:Driver.parse_tokens()`. This was a brilliant implementation
move because it does away with modifying the grammar to include all the
possible places in which comments can be placed.
The sad consequence for us though is that comments don't "belong" anywhere.
This is why this function generates simple parentless Leaf objects for
comments. We simply don't know what the correct parent should be.
No matter though, we can live without this. We really only need to
differentiate between inline and standalone comments. The latter don't
share the line with any code.
Inline comments are emitted as regular token.COMMENT leaves. Standalone
are emitted with a fake STANDALONE_COMMENT token identifier.
"""
total_consumed = 0
for pc in list_comments(
leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER, mode=mode
):
total_consumed = pc.consumed
prefix = make_simple_prefix(pc.newlines, pc.form_feed)
yield Leaf(pc.type, pc.value, prefix=prefix)
normalize_trailing_prefix(leaf, total_consumed)
@lru_cache(maxsize=4096)
def list_comments(prefix: str, *, is_endmarker: bool, mode: Mode) -> list[ProtoComment]:
"""Return a list of :class:`ProtoComment` objects parsed from the given `prefix`."""
result: list[ProtoComment] = []
if not prefix or "#" not in prefix:
return result
consumed = 0
nlines = 0
ignored_lines = 0
form_feed = False
for index, full_line in enumerate(re.split("\r?\n|\r", prefix)):
consumed += len(full_line) + 1 # adding the length of the split '\n'
match = re.match(r"^(\s*)(\S.*|)$", full_line)
assert match
whitespace, line = match.groups()
if not line:
nlines += 1
if "\f" in full_line:
form_feed = True
if not line.startswith("#"):
# Escaped newlines outside of a comment are not really newlines at
# all. We treat a single-line comment following an escaped newline
# as a simple trailing comment.
if line.endswith("\\"):
ignored_lines += 1
continue
if index == ignored_lines and not is_endmarker:
comment_type = token.COMMENT # simple trailing comment
else:
comment_type = STANDALONE_COMMENT
comment = make_comment(line, mode=mode)
result.append(
ProtoComment(
type=comment_type,
value=comment,
newlines=nlines,
consumed=consumed,
form_feed=form_feed,
leading_whitespace=whitespace,
)
)
form_feed = False
nlines = 0
return result
def normalize_trailing_prefix(leaf: LN, total_consumed: int) -> None:
"""Normalize the prefix that's left over after generating comments.
Note: don't use backslashes for formatting or you'll lose your voting rights.
"""
remainder = leaf.prefix[total_consumed:]
if "\\" not in remainder:
nl_count = remainder.count("\n")
form_feed = "\f" in remainder and remainder.endswith("\n")
leaf.prefix = make_simple_prefix(nl_count, form_feed)
return
leaf.prefix = ""
def make_comment(content: str, mode: Mode) -> str:
"""Return a consistently formatted comment from the given `content` string.
All comments (except for "##", "#!", "#:", '#'") should have a single
space between the hash sign and the content.
If `content` didn't start with a hash sign, one is provided.
Comments containing fmt directives are preserved exactly as-is to respect
user intent (e.g., `#no space # fmt: skip` stays as-is).
"""
content = content.rstrip()
if not content:
return "#"
# Preserve comments with fmt directives exactly as-is
if content.startswith("#") and contains_fmt_directive(content):
return content
if content[0] == "#":
content = content[1:]
if (
content
and content[0] == "\N{NO-BREAK SPACE}"
and not is_type_comment_string("# " + content.lstrip(), mode=mode)
):
content = " " + content[1:] # Replace NBSP by a simple space
if (
content
and "\N{NO-BREAK SPACE}" not in content
and is_type_comment_string("#" + content, mode=mode)
):
type_part, value_part = content.split(":", 1)
content = type_part.strip() + ": " + value_part.strip()
if content and content[0] not in COMMENT_EXCEPTIONS:
content = " " + content
return "#" + content
def normalize_fmt_off(
node: Node, mode: Mode, lines: Collection[tuple[int, int]]
) -> None:
"""Convert content between `# fmt: off`/`# fmt: on` into standalone comments."""
try_again = True
while try_again:
try_again = convert_one_fmt_off_pair(node, mode, lines)
def _should_process_fmt_comment(
comment: ProtoComment, leaf: Leaf
) -> tuple[bool, bool, bool]:
"""Check if comment should be processed for fmt handling.
Returns (should_process, is_fmt_off, is_fmt_skip).
"""
is_fmt_off = contains_fmt_directive(comment.value, FMT_OFF)
is_fmt_skip = contains_fmt_directive(comment.value, FMT_SKIP)
if not is_fmt_off and not is_fmt_skip:
return False, False, False
# Invalid use when `# fmt: off` is applied before a closing bracket
if is_fmt_off and leaf.type in CLOSING_BRACKETS:
return False, False, False
return True, is_fmt_off, is_fmt_skip
def _is_valid_standalone_fmt_comment(
comment: ProtoComment, leaf: Leaf, is_fmt_off: bool, is_fmt_skip: bool
) -> bool:
"""Check if comment is a valid standalone fmt directive.
We only want standalone comments. If there's no previous leaf or if
the previous leaf is indentation, it's a standalone comment in disguise.
"""
if comment.type == STANDALONE_COMMENT:
return True
prev = preceding_leaf(leaf)
if not prev:
return True
# Treat STANDALONE_COMMENT nodes as whitespace for check
if is_fmt_off and prev.type not in WHITESPACE and prev.type != STANDALONE_COMMENT:
return False
if is_fmt_skip and prev.type in WHITESPACE:
return False
return True
def _handle_comment_only_fmt_block(
leaf: Leaf,
comment: ProtoComment,
previous_consumed: int,
mode: Mode,
) -> bool:
"""Handle fmt:off/on blocks that contain only comments.
Returns True if a block was converted, False otherwise.
"""
all_comments = list_comments(leaf.prefix, is_endmarker=False, mode=mode)
# Find the first fmt:off and its matching fmt:on
fmt_off_idx = None
fmt_on_idx = None
for idx, c in enumerate(all_comments):
if fmt_off_idx is None and contains_fmt_directive(c.value, FMT_OFF):
fmt_off_idx = idx
if (
fmt_off_idx is not None
and idx > fmt_off_idx
and contains_fmt_directive(c.value, FMT_ON)
):
fmt_on_idx = idx
break
# Only proceed if we found both directives
if fmt_on_idx is None or fmt_off_idx is None:
return False
comment = all_comments[fmt_off_idx]
fmt_on_comment = all_comments[fmt_on_idx]
original_prefix = leaf.prefix
# Build the hidden value
start_pos = comment.consumed
end_pos = fmt_on_comment.consumed
content_between_and_fmt_on = original_prefix[start_pos:end_pos]
hidden_value = comment.value + "\n" + content_between_and_fmt_on
if hidden_value.endswith("\n"):
hidden_value = hidden_value[:-1]
# Build the standalone comment prefix - preserve all content before fmt:off
# including any comments that precede it
if fmt_off_idx == 0:
# No comments before fmt:off, use previous_consumed
pre_fmt_off_consumed = previous_consumed
else:
# Use the consumed position of the last comment before fmt:off
# This preserves all comments and content before the fmt:off directive
pre_fmt_off_consumed = all_comments[fmt_off_idx - 1].consumed
standalone_comment_prefix = (
original_prefix[:pre_fmt_off_consumed] + "\n" * comment.newlines
)
fmt_off_prefix = original_prefix.split(comment.value)[0]
if "\n" in fmt_off_prefix:
fmt_off_prefix = fmt_off_prefix.split("\n")[-1]
standalone_comment_prefix += fmt_off_prefix
# Update leaf prefix
leaf.prefix = original_prefix[fmt_on_comment.consumed :]
# Insert the STANDALONE_COMMENT
parent = leaf.parent
assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (prefix only)"
leaf_idx = None
for idx, child in enumerate(parent.children):
if child is leaf:
leaf_idx = idx
break
assert leaf_idx is not None, "INTERNAL ERROR: fmt: on/off handling (leaf index)"
parent.insert_child(
leaf_idx,
Leaf(
STANDALONE_COMMENT,
hidden_value,
prefix=standalone_comment_prefix,
fmt_pass_converted_first_leaf=None,
),
)
return True
def convert_one_fmt_off_pair(
node: Node, mode: Mode, lines: Collection[tuple[int, int]]
) -> bool:
"""Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment.
Returns True if a pair was converted.
"""
for leaf in node.leaves():
# Skip STANDALONE_COMMENT nodes that were created by fmt:off/on/skip processing
# to avoid reprocessing them in subsequent iterations
if leaf.type == STANDALONE_COMMENT and hasattr(
leaf, "fmt_pass_converted_first_leaf"
):
continue
previous_consumed = 0
for comment in list_comments(leaf.prefix, is_endmarker=False, mode=mode):
should_process, is_fmt_off, is_fmt_skip = _should_process_fmt_comment(
comment, leaf
)
if not should_process:
previous_consumed = comment.consumed
continue
if not _is_valid_standalone_fmt_comment(
comment, leaf, is_fmt_off, is_fmt_skip
):
previous_consumed = comment.consumed
continue
ignored_nodes = list(generate_ignored_nodes(leaf, comment, mode))
# Handle comment-only blocks
if not ignored_nodes and is_fmt_off:
if _handle_comment_only_fmt_block(
leaf, comment, previous_consumed, mode
):
return True
continue
# Need actual nodes to process
if not ignored_nodes:
continue
# Handle regular fmt blocks
_handle_regular_fmt_block(
ignored_nodes,
comment,
previous_consumed,
is_fmt_skip,
lines,
leaf,
)
return True
return False
def _handle_regular_fmt_block(
ignored_nodes: list[LN],
comment: ProtoComment,
previous_consumed: int,
is_fmt_skip: bool,
lines: Collection[tuple[int, int]],
leaf: Leaf,
) -> None:
"""Handle fmt blocks with actual AST nodes."""
first = ignored_nodes[0] # Can be a container node with the `leaf`.
parent = first.parent
prefix = first.prefix
if contains_fmt_directive(comment.value, FMT_OFF):
first.prefix = prefix[comment.consumed :]
if is_fmt_skip:
first.prefix = ""
standalone_comment_prefix = prefix
else:
standalone_comment_prefix = prefix[:previous_consumed] + "\n" * comment.newlines
# Ensure STANDALONE_COMMENT nodes have trailing newlines when stringified
# This prevents multiple fmt: skip comments from being concatenated on one line
parts = []
for node in ignored_nodes:
if isinstance(node, Leaf) and node.type == STANDALONE_COMMENT:
# Add newline after STANDALONE_COMMENT Leaf
node_str = str(node)
if not node_str.endswith("\n"):
node_str += "\n"
parts.append(node_str)
elif isinstance(node, Node):
# For nodes that might contain STANDALONE_COMMENT leaves,
# we need custom stringify
has_standalone = any(
leaf.type == STANDALONE_COMMENT for leaf in node.leaves()
)
if has_standalone:
# Stringify node with STANDALONE_COMMENT leaves having trailing newlines
def stringify_node(n: LN) -> str:
if isinstance(n, Leaf):
if n.type == STANDALONE_COMMENT:
result = n.prefix + n.value
if not result.endswith("\n"):
result += "\n"
return result
return str(n)
else:
# For nested nodes, recursively process children
return "".join(stringify_node(child) for child in n.children)
parts.append(stringify_node(node))
else:
parts.append(str(node))
else:
parts.append(str(node))
hidden_value = "".join(parts)
comment_lineno = leaf.lineno - comment.newlines
if contains_fmt_directive(comment.value, FMT_OFF):
fmt_off_prefix = ""
if len(lines) > 0 and not any(
line[0] <= comment_lineno <= line[1] for line in lines
):
# keeping indentation of comment by preserving original whitespaces.
fmt_off_prefix = prefix.split(comment.value)[0]
if "\n" in fmt_off_prefix:
fmt_off_prefix = fmt_off_prefix.split("\n")[-1]
standalone_comment_prefix += fmt_off_prefix
hidden_value = comment.value + "\n" + hidden_value
if is_fmt_skip:
hidden_value += comment.leading_whitespace + comment.value
if hidden_value.endswith("\n"):
# That happens when one of the `ignored_nodes` ended with a NEWLINE
# leaf (possibly followed by a DEDENT).
hidden_value = hidden_value[:-1]
first_idx: int | None = None
for ignored in ignored_nodes:
index = ignored.remove()
if first_idx is None:
first_idx = index
assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (1)"
assert first_idx is not None, "INTERNAL ERROR: fmt: on/off handling (2)"
parent.insert_child(
first_idx,
Leaf(
STANDALONE_COMMENT,
hidden_value,
prefix=standalone_comment_prefix,
fmt_pass_converted_first_leaf=first_leaf_of(first),
),
)
def generate_ignored_nodes(
leaf: Leaf, comment: ProtoComment, mode: Mode
) -> Iterator[LN]:
"""Starting from the container of `leaf`, generate all leaves until `# fmt: on`.
If comment is skip, returns leaf only.
Stops at the end of the block.
"""
if contains_fmt_directive(comment.value, FMT_SKIP):
yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment, mode)
return
container: LN | None = container_of(leaf)
while container is not None and container.type != token.ENDMARKER:
if is_fmt_on(container, mode=mode):
return
# fix for fmt: on in children
if children_contains_fmt_on(container, mode=mode):
for index, child in enumerate(container.children):
if isinstance(child, Leaf) and is_fmt_on(child, mode=mode):
if child.type in CLOSING_BRACKETS:
# This means `# fmt: on` is placed at a different bracket level
# than `# fmt: off`. This is an invalid use, but as a courtesy,
# we include this closing bracket in the ignored nodes.
# The alternative is to fail the formatting.
yield child
return
if (
child.type == token.INDENT
and index < len(container.children) - 1
and children_contains_fmt_on(
container.children[index + 1], mode=mode
)
):
# This means `# fmt: on` is placed right after an indentation
# level, and we shouldn't swallow the previous INDENT token.
return
if children_contains_fmt_on(child, mode=mode):
return
yield child
else:
if container.type == token.DEDENT and container.next_sibling is None:
# This can happen when there is no matching `# fmt: on` comment at the
# same level as `# fmt: on`. We need to keep this DEDENT.
return
yield container
container = container.next_sibling
def _find_compound_statement_context(parent: Node) -> Node | None:
"""Return the body node of a compound statement if we should respect fmt: skip.
This handles one-line compound statements like:
if condition: body # fmt: skip
When Black expands such statements, they temporarily look like:
if condition:
body # fmt: skip
In both cases, we want to return the body node (either the simple_stmt directly
or the suite containing it).
"""
if parent.type != syms.simple_stmt:
return None
if not isinstance(parent.parent, Node):
return None
# Case 1: Expanded form after Black's initial formatting pass.
# The one-liner has been split across multiple lines:
# if True:
# print("a"); print("b") # fmt: skip
# Structure: compound_stmt -> suite -> simple_stmt
if (
parent.parent.type == syms.suite
and isinstance(parent.parent.parent, Node)
and parent.parent.parent.type in _COMPOUND_STATEMENTS
):
return parent.parent
# Case 2: Original one-line form from the input source.
# The statement is still on a single line:
# if True: print("a"); print("b") # fmt: skip
# Structure: compound_stmt -> simple_stmt
if parent.parent.type in _COMPOUND_STATEMENTS:
return parent
return None
def _should_keep_compound_statement_inline(
body_node: Node, simple_stmt_parent: Node
) -> bool:
"""Check if a compound statement should be kept on one line.
Returns True only for compound statements with semicolon-separated bodies,
like: if True: print("a"); print("b") # fmt: skip
"""
# Check if there are semicolons in the body
for leaf in body_node.leaves():
if leaf.type == token.SEMI:
# Verify it's a single-line body (one simple_stmt)
if body_node.type == syms.suite:
# After formatting: check suite has one simple_stmt child
simple_stmts = [
child
for child in body_node.children
if child.type == syms.simple_stmt
]
return len(simple_stmts) == 1 and simple_stmts[0] is simple_stmt_parent
else:
# Original form: body_node IS the simple_stmt
return body_node is simple_stmt_parent
return False
def _get_compound_statement_header(
body_node: Node, simple_stmt_parent: Node
) -> list[LN]:
"""Get header nodes for a compound statement that should be preserved inline."""
if not _should_keep_compound_statement_inline(body_node, simple_stmt_parent):
return []
# Get the compound statement (parent of body)
compound_stmt = body_node.parent
if compound_stmt is None or compound_stmt.type not in _COMPOUND_STATEMENTS:
return []
# Collect all header leaves before the body
header_leaves: list[LN] = []
for child in compound_stmt.children:
if child is body_node:
break
if isinstance(child, Leaf):
if child.type not in (token.NEWLINE, token.INDENT):
header_leaves.append(child)
else:
header_leaves.extend(child.leaves())
return header_leaves
def _generate_ignored_nodes_from_fmt_skip(
leaf: Leaf, comment: ProtoComment, mode: Mode
) -> Iterator[LN]:
"""Generate all leaves that should be ignored by the `# fmt: skip` from `leaf`."""
prev_sibling = leaf.prev_sibling
parent = leaf.parent
ignored_nodes: list[LN] = []
# Need to properly format the leaf prefix to compare it to comment.value,
# which is also formatted
comments = list_comments(leaf.prefix, is_endmarker=False, mode=mode)
if not comments or comment.value != comments[0].value:
return
if not prev_sibling and parent:
prev_sibling = parent.prev_sibling
if prev_sibling is not None:
leaf.prefix = leaf.prefix[comment.consumed :]
# Generates the nodes to be ignored by `fmt: skip`.
# Nodes to ignore are the ones on the same line as the
# `# fmt: skip` comment, excluding the `# fmt: skip`
# node itself.
# Traversal process (starting at the `# fmt: skip` node):
# 1. Move to the `prev_sibling` of the current node.
# 2. If `prev_sibling` has children, go to its rightmost leaf.
# 3. If there's no `prev_sibling`, move up to the parent
# node and repeat.
# 4. Continue until:
# a. You encounter an `INDENT` or `NEWLINE` node (indicates
# start of the line).
# b. You reach the root node.
# Include all visited LEAVES in the ignored list, except INDENT
# or NEWLINE leaves.
current_node = prev_sibling
ignored_nodes = [current_node]
if current_node.prev_sibling is None and current_node.parent is not None:
current_node = current_node.parent
# Track seen nodes to detect cycles that can occur after tree modifications
seen_nodes = {id(current_node)}
while "\n" not in current_node.prefix and current_node.prev_sibling is not None:
leaf_nodes = list(current_node.prev_sibling.leaves())
next_node = leaf_nodes[-1] if leaf_nodes else current_node
# Detect infinite loop - if we've seen this node before, stop
# This can happen when STANDALONE_COMMENT nodes are inserted
# during processing
if id(next_node) in seen_nodes:
break
current_node = next_node
seen_nodes.add(id(current_node))
# Stop if we encounter a STANDALONE_COMMENT created by fmt processing
if (
isinstance(current_node, Leaf)
and current_node.type == STANDALONE_COMMENT
and hasattr(current_node, "fmt_pass_converted_first_leaf")
):
break
if (
current_node.type in CLOSING_BRACKETS
and current_node.parent
and current_node.parent.type == syms.atom
):
current_node = current_node.parent
if current_node.type in (token.NEWLINE, token.INDENT):
current_node.prefix = ""
break
if current_node.type == token.DEDENT:
break
# Special case for with expressions
# Without this, we can stuck inside the asexpr_test's children's children
if (
current_node.parent
and current_node.parent.type == syms.asexpr_test
and current_node.parent.parent
and current_node.parent.parent.type == syms.with_stmt
):
current_node = current_node.parent
ignored_nodes.insert(0, current_node)
if current_node.prev_sibling is None and current_node.parent is not None:
current_node = current_node.parent
# Special handling for compound statements with semicolon-separated bodies
if isinstance(parent, Node):
body_node = _find_compound_statement_context(parent)
if body_node is not None:
header_nodes = _get_compound_statement_header(body_node, parent)
if header_nodes:
ignored_nodes = header_nodes + ignored_nodes
yield from ignored_nodes
elif (
parent is not None and parent.type == syms.suite and leaf.type == token.NEWLINE
):
# The `# fmt: skip` is on the colon line of the if/while/def/class/...
# statements. The ignored nodes should be previous siblings of the
# parent suite node.
leaf.prefix = ""
parent_sibling = parent.prev_sibling
while parent_sibling is not None and parent_sibling.type != syms.suite:
ignored_nodes.insert(0, parent_sibling)
parent_sibling = parent_sibling.prev_sibling
# Special case for `async_stmt` where the ASYNC token is on the
# grandparent node.
grandparent = parent.parent
if (
grandparent is not None
and grandparent.prev_sibling is not None
and grandparent.prev_sibling.type == token.ASYNC
):
ignored_nodes.insert(0, grandparent.prev_sibling)
yield from iter(ignored_nodes)
def is_fmt_on(container: LN, mode: Mode) -> bool:
"""Determine whether formatting is switched on within a container.
Determined by whether the last `# fmt:` comment is `on` or `off`.
"""
fmt_on = False
for comment in list_comments(container.prefix, is_endmarker=False, mode=mode):
if contains_fmt_directive(comment.value, FMT_ON):
fmt_on = True
elif contains_fmt_directive(comment.value, FMT_OFF):
fmt_on = False
return fmt_on
def children_contains_fmt_on(container: LN, mode: Mode) -> bool:
"""Determine if children have formatting switched on."""
for child in container.children:
leaf = first_leaf_of(child)
if leaf is not None and is_fmt_on(leaf, mode=mode):
return True
return False
def contains_pragma_comment(comment_list: list[Leaf]) -> bool:
"""
Returns:
True iff one of the comments in @comment_list is a pragma used by one
of the more common static analysis tools for python (e.g. mypy, flake8,
pylint).
"""
for comment in comment_list:
if comment.value.startswith(("# type:", "# noqa", "# pylint:")):
return True
return False
def contains_fmt_directive(
comment_line: str, directives: set[str] = FMT_OFF | FMT_ON | FMT_SKIP
) -> bool:
"""
Checks if the given comment contains format directives, alone or paired with
other comments.
Defaults to checking all directives (skip, off, on, yapf), but can be
narrowed to specific ones.
Matching styles:
# foobar <-- single comment
# foobar # foobar # foobar <-- multiple comments
# foobar; foobar <-- list of comments (; separated)
"""
semantic_comment_blocks = [
comment_line,
*[
_COMMENT_PREFIX + comment.strip()
for comment in comment_line.split(_COMMENT_PREFIX)[1:]
],
*[
_COMMENT_PREFIX + comment.strip()
for comment in comment_line.strip(_COMMENT_PREFIX).split(
_COMMENT_LIST_SEPARATOR
)
],
]
return any(comment in directives for comment in semantic_comment_blocks)

View file

@ -0,0 +1,221 @@
"""
Formatting many files at once via multiprocessing. Contains entrypoint and utilities.
NOTE: this module is only imported if we need to format several files at once.
"""
from __future__ import annotations
import asyncio
import logging
import os
import signal
import sys
import traceback
from collections.abc import Iterable
from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor
from multiprocessing import Manager
from pathlib import Path
from typing import Any
from mypy_extensions import mypyc_attr
from black import WriteBack, format_file_in_place
from black.cache import Cache
from black.mode import Mode
from black.output import err
from black.report import Changed, Report
def maybe_use_uvloop() -> asyncio.AbstractEventLoop:
"""If our environment has uvloop or winloop installed we use it otherwise
a normal asyncio eventloop is called as fallback.
This is called only from command-line entry points to avoid
interfering with the parent process if Black is used as a library.
"""
try:
if sys.platform != "win32":
import uvloop
return uvloop.new_event_loop()
else:
import winloop
return winloop.new_event_loop()
except ImportError:
return asyncio.new_event_loop()
def cancel(tasks: Iterable[asyncio.Future[Any]]) -> None:
"""asyncio signal handler that cancels all `tasks` and reports to stderr."""
err("Aborted!")
for task in tasks:
task.cancel()
def shutdown(loop: asyncio.AbstractEventLoop) -> None:
"""Cancel all pending tasks on `loop`, wait for them, and close the loop."""
try:
# This part is borrowed from asyncio/runners.py in Python 3.7b2.
to_cancel = [task for task in asyncio.all_tasks(loop) if not task.done()]
if not to_cancel:
return
for task in to_cancel:
task.cancel()
loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True))
finally:
# `concurrent.futures.Future` objects cannot be cancelled once they
# are already running. There might be some when the `shutdown()` happened.
# Silence their logger's spew about the event loop being closed.
cf_logger = logging.getLogger("concurrent.futures")
cf_logger.setLevel(logging.CRITICAL)
loop.close()
# diff-shades depends on being to monkeypatch this function to operate. I know it's
# not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26
@mypyc_attr(patchable=True)
def reformat_many(
sources: set[Path],
fast: bool,
write_back: WriteBack,
mode: Mode,
report: Report,
workers: int | None,
no_cache: bool = False,
) -> None:
"""Reformat multiple files using a ProcessPoolExecutor."""
if workers is None:
workers = int(os.environ.get("BLACK_NUM_WORKERS", 0))
workers = workers or os.cpu_count() or 1
if sys.platform == "win32":
# Work around https://bugs.python.org/issue26903
workers = min(workers, 60)
if getattr(sys, "frozen", False):
# In frozen builds (e.g. PyInstaller), avoid spawning worker processes (i.e.
# avoid using ProcessPoolExecutor) to prevent shutdown errors when workers
# try to import modules after cleanup begins.
# See https://github.com/psf/black/issues/4823
workers = 1
executor: Executor | None = None
if workers > 1:
try:
executor = ProcessPoolExecutor(max_workers=workers)
except (ImportError, NotImplementedError, OSError):
# we arrive here if the underlying system does not support multi-processing
# like in AWS Lambda or Termux, in which case we gracefully fallback to
# a ThreadPoolExecutor with just a single worker (more workers would not do
# us any good due to the Global Interpreter Lock)
pass
if executor is None:
executor = ThreadPoolExecutor(max_workers=1)
loop = maybe_use_uvloop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(
schedule_formatting(
sources=sources,
fast=fast,
write_back=write_back,
mode=mode,
report=report,
loop=loop,
executor=executor,
no_cache=no_cache,
)
)
finally:
try:
shutdown(loop)
finally:
asyncio.set_event_loop(None)
if executor is not None:
executor.shutdown()
async def schedule_formatting(
sources: set[Path],
fast: bool,
write_back: WriteBack,
mode: Mode,
report: Report,
loop: asyncio.AbstractEventLoop,
executor: Executor,
no_cache: bool = False,
) -> None:
"""Run formatting of `sources` in parallel using the provided `executor`.
(Use ProcessPoolExecutors for actual parallelism.)
`write_back`, `fast`, and `mode` options are passed to
:func:`format_file_in_place`.
"""
cache = None if no_cache else Cache.read(mode)
if cache is not None and write_back not in (
WriteBack.DIFF,
WriteBack.COLOR_DIFF,
):
sources, cached = cache.filtered_cached(sources)
for src in sorted(cached):
report.done(src, Changed.CACHED)
if not sources:
return
cancelled = []
sources_to_cache = []
lock = None
manager = None
if write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
# For diff output, we need locks to ensure we don't interleave output
# from different processes.
manager = Manager()
lock = manager.Lock()
try:
tasks = {
asyncio.ensure_future(
loop.run_in_executor(
executor, format_file_in_place, src, fast, mode, write_back, lock
)
): src
for src in sorted(sources)
}
pending = tasks.keys()
try:
loop.add_signal_handler(signal.SIGINT, cancel, pending)
loop.add_signal_handler(signal.SIGTERM, cancel, pending)
except NotImplementedError:
# There are no good alternatives for these on Windows.
pass
while pending:
done, _ = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
for task in done:
src = tasks.pop(task)
if task.cancelled():
cancelled.append(task)
elif exc := task.exception():
if report.verbose:
traceback.print_exception(type(exc), exc, exc.__traceback__)
report.failed(src, str(exc))
else:
changed = Changed.YES if task.result() else Changed.NO
# If the file was written back or was successfully checked as
# well-formatted, store this information in the cache.
if write_back is WriteBack.YES or (
write_back is WriteBack.CHECK and changed is Changed.NO
):
sources_to_cache.append(src)
report.done(src, changed)
if cancelled:
await asyncio.gather(*cancelled, return_exceptions=True)
if sources_to_cache and not no_cache and cache is not None:
cache.write(sources_to_cache)
finally:
if manager is not None:
manager.shutdown()

View file

@ -0,0 +1,4 @@
DEFAULT_LINE_LENGTH = 88
DEFAULT_EXCLUDES = r"/(\.direnv|\.eggs|\.git|\.hg|\.ipynb_checkpoints|\.mypy_cache|\.nox|\.pytest_cache|\.ruff_cache|\.tox|\.svn|\.venv|\.vscode|__pypackages__|_build|buck-out|build|dist|venv)/" # noqa: B950
DEFAULT_INCLUDES = r"(\.pyi?|\.ipynb)$"
STDIN_PLACEHOLDER = "__BLACK_STDIN_FILENAME__"

View file

@ -0,0 +1,55 @@
from collections.abc import Iterator
from dataclasses import dataclass, field
from typing import Any, TypeVar, Union
from black.nodes import Visitor
from black.output import out
from black.parsing import lib2to3_parse
from blib2to3.pgen2 import token
from blib2to3.pytree import Leaf, Node, type_repr
LN = Union[Leaf, Node]
T = TypeVar("T")
@dataclass
class DebugVisitor(Visitor[T]):
tree_depth: int = 0
list_output: list[str] = field(default_factory=list)
print_output: bool = True
def out(self, message: str, *args: Any, **kwargs: Any) -> None:
self.list_output.append(message)
if self.print_output:
out(message, *args, **kwargs)
def visit_default(self, node: LN) -> Iterator[T]:
indent = " " * (2 * self.tree_depth)
if isinstance(node, Node):
_type = type_repr(node.type)
self.out(f"{indent}{_type}", fg="yellow")
self.tree_depth += 1
for child in node.children:
yield from self.visit(child)
self.tree_depth -= 1
self.out(f"{indent}/{_type}", fg="yellow", bold=False)
else:
_type = token.tok_name.get(node.type, str(node.type))
self.out(f"{indent}{_type}", fg="blue", nl=False)
if node.prefix:
# We don't have to handle prefixes for `Node` objects since
# that delegates to the first child anyway.
self.out(f" {node.prefix!r}", fg="green", bold=False, nl=False)
self.out(f" {node.value!r}", fg="blue", bold=False)
@classmethod
def show(cls, code: str | Leaf | Node) -> None:
"""Pretty-print the lib2to3 AST of a given string of `code`.
Convenience method for debugging.
"""
v: DebugVisitor[None] = DebugVisitor()
if isinstance(code, str):
code = lib2to3_parse(code)
list(v.visit(code))

View file

@ -0,0 +1,426 @@
import io
import os
import sys
from collections.abc import Iterable, Iterator, Sequence
from functools import lru_cache
from pathlib import Path
from re import Pattern
from typing import TYPE_CHECKING, Any, Union
from mypy_extensions import mypyc_attr
from packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet
from packaging.version import InvalidVersion, Version
from pathspec import GitIgnoreSpec
from pathspec.patterns.gitignore import GitIgnorePatternError
if sys.version_info >= (3, 11):
try:
import tomllib
except ImportError:
# Help users on older alphas
if not TYPE_CHECKING:
import tomli as tomllib
else:
import tomli as tomllib
from black.handle_ipynb_magics import jupyter_dependencies_are_installed
from black.mode import TargetVersion
from black.output import err
from black.report import Report
if TYPE_CHECKING:
import colorama
@lru_cache
def _load_toml(path: Path | str) -> dict[str, Any]:
with open(path, "rb") as f:
return tomllib.load(f)
@lru_cache
def _cached_resolve(path: Path) -> Path:
return path.resolve()
@lru_cache
def find_project_root(
srcs: Sequence[str], stdin_filename: str | None = None
) -> tuple[Path, str]:
"""Return a directory containing .git, .hg, or pyproject.toml.
pyproject.toml files are only considered if they contain a [tool.black]
section and are ignored otherwise.
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.
Returns a two-tuple with the first element as the project root path and
the second element as a string describing the method by which the
project root was discovered.
"""
if stdin_filename is not None:
srcs = tuple(stdin_filename if s == "-" else s for s in srcs)
if not srcs:
srcs = [str(_cached_resolve(Path.cwd()))]
path_srcs = [_cached_resolve(Path(Path.cwd(), src)) 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, ".git directory"
if (directory / ".hg").is_dir():
return directory, ".hg directory"
if (directory / "pyproject.toml").is_file():
pyproject_toml = _load_toml(directory / "pyproject.toml")
if "black" in pyproject_toml.get("tool", {}):
return directory, "pyproject.toml"
return directory, "file system root"
def find_pyproject_toml(
path_search_start: tuple[str, ...], stdin_filename: str | None = None
) -> str | None:
"""Find the absolute filepath to a pyproject.toml if it exists"""
path_project_root, _ = find_project_root(path_search_start, stdin_filename)
path_pyproject_toml = path_project_root / "pyproject.toml"
if path_pyproject_toml.is_file():
return str(path_pyproject_toml)
try:
path_user_pyproject_toml = find_user_pyproject_toml()
return (
str(path_user_pyproject_toml)
if path_user_pyproject_toml.is_file()
else None
)
except (PermissionError, RuntimeError) as e:
# We do not have access to the user-level config directory, so ignore it.
err(f"Ignoring user configuration directory due to {e!r}")
return None
@mypyc_attr(patchable=True)
def parse_pyproject_toml(path_config: str) -> dict[str, Any]:
"""Parse a pyproject toml file, pulling out relevant parts for Black.
If parsing fails, will raise a tomllib.TOMLDecodeError.
"""
pyproject_toml = _load_toml(path_config)
config: dict[str, Any] = pyproject_toml.get("tool", {}).get("black", {})
config = {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}
if "target_version" not in config:
inferred_target_version = infer_target_version(pyproject_toml)
if inferred_target_version is not None:
config["target_version"] = [v.name.lower() for v in inferred_target_version]
return config
def infer_target_version(
pyproject_toml: dict[str, Any],
) -> list[TargetVersion] | None:
"""Infer Black's target version from the project metadata in pyproject.toml.
Supports the PyPA standard format (PEP 621):
https://packaging.python.org/en/latest/specifications/declaring-project-metadata/#requires-python
If the target version cannot be inferred, returns None.
"""
project_metadata = pyproject_toml.get("project", {})
requires_python = project_metadata.get("requires-python", None)
if requires_python is not None:
try:
return parse_req_python_version(requires_python)
except InvalidVersion:
pass
try:
return parse_req_python_specifier(requires_python)
except (InvalidSpecifier, InvalidVersion):
pass
return None
def parse_req_python_version(requires_python: str) -> list[TargetVersion] | None:
"""Parse a version string (i.e. ``"3.7"``) to a list of TargetVersion.
If parsing fails, will raise a packaging.version.InvalidVersion error.
If the parsed version cannot be mapped to a valid TargetVersion, returns None.
"""
version = Version(requires_python)
if version.release[0] != 3:
return None
try:
return [TargetVersion(version.release[1])]
except (IndexError, ValueError):
return None
def parse_req_python_specifier(requires_python: str) -> list[TargetVersion] | None:
"""Parse a specifier string (i.e. ``">=3.7,<3.10"``) to a list of TargetVersion.
If parsing fails, will raise a packaging.specifiers.InvalidSpecifier error.
If the parsed specifier cannot be mapped to a valid TargetVersion, returns None.
"""
specifier_set = strip_specifier_set(SpecifierSet(requires_python))
if not specifier_set:
return None
target_version_map = {f"3.{v.value}": v for v in TargetVersion}
compatible_versions: list[str] = list(specifier_set.filter(target_version_map))
if compatible_versions:
return [target_version_map[v] for v in compatible_versions]
return None
def strip_specifier_set(specifier_set: SpecifierSet) -> SpecifierSet:
"""Strip minor versions for some specifiers in the specifier set.
For background on version specifiers, see PEP 440:
https://peps.python.org/pep-0440/#version-specifiers
"""
specifiers = []
for s in specifier_set:
if "*" in str(s):
specifiers.append(s)
elif s.operator in ["~=", "==", ">=", "==="]:
version = Version(s.version)
stripped = Specifier(f"{s.operator}{version.major}.{version.minor}")
specifiers.append(stripped)
elif s.operator == ">":
version = Version(s.version)
if len(version.release) > 2:
s = Specifier(f">={version.major}.{version.minor}")
specifiers.append(s)
else:
specifiers.append(s)
return SpecifierSet(",".join(str(s) for s in specifiers))
@lru_cache
def find_user_pyproject_toml() -> Path:
r"""Return the path to the top-level user configuration for black.
This looks for ~\.black on Windows and ~/.config/black on Linux and other
Unix systems.
May raise:
- RuntimeError: if the current user has no homedir
- PermissionError: if the current process cannot access the user's homedir
"""
if sys.platform == "win32":
# Windows
user_config_path = Path.home() / ".black"
else:
config_root = os.environ.get("XDG_CONFIG_HOME", "~/.config")
user_config_path = Path(config_root).expanduser() / "black"
return _cached_resolve(user_config_path)
@lru_cache
def get_gitignore(root: Path) -> GitIgnoreSpec:
"""Return a GitIgnoreSpec matching gitignore content if present."""
gitignore = root / ".gitignore"
lines: list[str] = []
if gitignore.is_file():
with gitignore.open(encoding="utf-8") as gf:
lines = gf.readlines()
try:
return GitIgnoreSpec.from_lines(lines)
except GitIgnorePatternError as e:
err(f"Could not parse {gitignore}: {e}")
raise
def resolves_outside_root_or_cannot_stat(
path: Path,
root: Path,
report: Report | None = None,
) -> bool:
"""
Returns whether the path is a symbolic link that points outside the
root directory. Also returns True if we failed to resolve the path.
"""
try:
resolved_path = _cached_resolve(path)
except OSError as e:
if report:
report.path_ignored(path, f"cannot be read because {e}")
return True
try:
resolved_path.relative_to(root)
except ValueError:
if report:
report.path_ignored(path, f"is a symbolic link that points outside {root}")
return True
return False
def best_effort_relative_path(path: Path, root: Path) -> Path:
# Precondition: resolves_outside_root_or_cannot_stat(path, root) is False
try:
return path.absolute().relative_to(root)
except ValueError:
pass
root_parent = next((p for p in path.parents if _cached_resolve(p) == root), None)
if root_parent is not None:
return path.relative_to(root_parent)
# something adversarial, fallback to path guaranteed by precondition
return _cached_resolve(path).relative_to(root)
def _path_is_ignored(
root_relative_path: str,
root: Path,
gitignore_dict: dict[Path, GitIgnoreSpec],
) -> bool:
path = root / root_relative_path
# Note that this logic is sensitive to the ordering of gitignore_dict. Callers must
# ensure that gitignore_dict is ordered from least specific to most specific.
for gitignore_path, pattern in gitignore_dict.items():
try:
relative_path = path.relative_to(gitignore_path).as_posix()
if path.is_dir():
relative_path = relative_path + "/"
except ValueError:
break
if pattern.match_file(relative_path):
return True
return False
def path_is_excluded(
normalized_path: str,
pattern: Pattern[str] | None,
) -> bool:
match = pattern.search(normalized_path) if pattern else None
return bool(match and match.group(0))
def gen_python_files(
paths: Iterable[Path],
root: Path,
include: Pattern[str],
exclude: Pattern[str],
extend_exclude: Pattern[str] | None,
force_exclude: Pattern[str] | None,
report: Report,
gitignore_dict: dict[Path, GitIgnoreSpec] | None,
*,
verbose: bool,
quiet: bool,
) -> Iterator[Path]:
"""Generate all files under `path` whose paths are not excluded by the
`exclude_regex`, `extend_exclude`, or `force_exclude` regexes,
but are included by the `include` regex.
Symbolic links pointing outside of the `root` directory are ignored.
`report` is where output about exclusions goes.
"""
assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}"
for child in paths:
assert child.is_absolute()
root_relative_path = child.relative_to(root).as_posix()
# First ignore files matching .gitignore, if passed
if gitignore_dict and _path_is_ignored(
root_relative_path, root, gitignore_dict
):
report.path_ignored(child, "matches a .gitignore file content")
continue
# Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options.
root_relative_path = "/" + root_relative_path
if child.is_dir():
root_relative_path += "/"
if path_is_excluded(root_relative_path, exclude):
report.path_ignored(child, "matches the --exclude regular expression")
continue
if path_is_excluded(root_relative_path, extend_exclude):
report.path_ignored(
child, "matches the --extend-exclude regular expression"
)
continue
if path_is_excluded(root_relative_path, force_exclude):
report.path_ignored(child, "matches the --force-exclude regular expression")
continue
if resolves_outside_root_or_cannot_stat(child, root, report):
continue
if child.is_dir():
# If gitignore is None, gitignore usage is disabled, while a Falsey
# gitignore is when the directory doesn't have a .gitignore file.
if gitignore_dict is not None:
new_gitignore_dict = {
**gitignore_dict,
root / child: get_gitignore(child),
}
else:
new_gitignore_dict = None
yield from gen_python_files(
child.iterdir(),
root,
include,
exclude,
extend_exclude,
force_exclude,
report,
new_gitignore_dict,
verbose=verbose,
quiet=quiet,
)
elif child.is_file():
if child.suffix == ".ipynb" and not jupyter_dependencies_are_installed(
warn=verbose or not quiet
):
continue
include_match = include.search(root_relative_path) if include else True
if include_match:
yield child
def wrap_stream_for_windows(
f: io.TextIOWrapper,
) -> Union[io.TextIOWrapper, "colorama.AnsiToWin32"]:
"""
Wrap stream with colorama's wrap_stream so colors are shown on Windows.
If `colorama` is unavailable, the original stream is returned unmodified.
Otherwise, the `wrap_stream()` function determines whether the stream needs
to be wrapped for a Windows environment and will accordingly either return
an `AnsiToWin32` wrapper or the original stream.
"""
try:
from colorama.initialise import wrap_stream
except ImportError:
return f
else:
# Set `strip=False` to avoid needing to modify test_express_diff_with_color.
return wrap_stream(f, convert=None, strip=False, autoreset=False, wrap=True)

View file

@ -0,0 +1,515 @@
"""Functions to process IPython magics with."""
import ast
import collections
import dataclasses
import re
import secrets
import string
from collections.abc import Collection
from functools import lru_cache
from importlib.util import find_spec
from typing import TypeGuard
from black.mode import Mode
from black.output import out
from black.report import NothingChanged
TRANSFORMED_MAGICS = frozenset((
"get_ipython().run_cell_magic",
"get_ipython().system",
"get_ipython().getoutput",
"get_ipython().run_line_magic",
))
TOKENS_TO_IGNORE = frozenset((
"ENDMARKER",
"NL",
"NEWLINE",
"COMMENT",
"DEDENT",
"UNIMPORTANT_WS",
"ESCAPED_NL",
))
PYTHON_CELL_MAGICS = frozenset((
"capture",
"prun",
"pypy",
"python",
"python3",
"time",
"timeit",
))
@dataclasses.dataclass(frozen=True)
class Replacement:
mask: str
src: str
@lru_cache
def jupyter_dependencies_are_installed(*, warn: bool) -> bool:
installed = (
find_spec("tokenize_rt") is not None and find_spec("IPython") is not None
)
if not installed and warn:
msg = (
"Skipping .ipynb files as Jupyter dependencies are not installed.\n"
'You can fix this by running ``pip install "black[jupyter]"``'
)
out(msg)
return installed
def validate_cell(src: str, mode: Mode) -> None:
r"""Check that cell does not already contain TransformerManager transformations,
or non-Python cell magics, which might cause tokenizer_rt to break because of
indentations.
If a cell contains ``!ls``, then it'll be transformed to
``get_ipython().system('ls')``. However, if the cell originally contained
``get_ipython().system('ls')``, then it would get transformed in the same way:
>>> TransformerManager().transform_cell("get_ipython().system('ls')")
"get_ipython().system('ls')\n"
>>> TransformerManager().transform_cell("!ls")
"get_ipython().system('ls')\n"
Due to the impossibility of safely roundtripping in such situations, cells
containing transformed magics will be ignored.
"""
if any(transformed_magic in src for transformed_magic in TRANSFORMED_MAGICS):
raise NothingChanged
line = _get_code_start(src)
if line.startswith("%%") and (
line.split(maxsplit=1)[0][2:]
not in PYTHON_CELL_MAGICS | mode.python_cell_magics
):
raise NothingChanged
def remove_trailing_semicolon(src: str) -> tuple[str, bool]:
"""Remove trailing semicolon from Jupyter notebook cell.
For example,
fig, ax = plt.subplots()
ax.plot(x_data, y_data); # plot data
would become
fig, ax = plt.subplots()
ax.plot(x_data, y_data) # plot data
Mirrors the logic in `quiet` from `IPython.core.displayhook`, but uses
``tokenize_rt`` so that round-tripping works fine.
"""
from tokenize_rt import reversed_enumerate, src_to_tokens, tokens_to_src
tokens = src_to_tokens(src)
trailing_semicolon = False
for idx, token in reversed_enumerate(tokens):
if token.name in TOKENS_TO_IGNORE:
continue
if token.name == "OP" and token.src == ";":
del tokens[idx]
trailing_semicolon = True
break
if not trailing_semicolon:
return src, False
return tokens_to_src(tokens), True
def put_trailing_semicolon_back(src: str, has_trailing_semicolon: bool) -> str:
"""Put trailing semicolon back if cell originally had it.
Mirrors the logic in `quiet` from `IPython.core.displayhook`, but uses
``tokenize_rt`` so that round-tripping works fine.
"""
if not has_trailing_semicolon:
return src
from tokenize_rt import reversed_enumerate, src_to_tokens, tokens_to_src
tokens = src_to_tokens(src)
for idx, token in reversed_enumerate(tokens):
if token.name in TOKENS_TO_IGNORE:
continue
tokens[idx] = token._replace(src=token.src + ";")
break
else: # pragma: nocover
raise AssertionError(
"INTERNAL ERROR: Was not able to reinstate trailing semicolon. "
"Please report a bug on https://github.com/psf/black/issues. "
) from None
return str(tokens_to_src(tokens))
def mask_cell(src: str) -> tuple[str, list[Replacement]]:
"""Mask IPython magics so content becomes parseable Python code.
For example,
%matplotlib inline
'foo'
becomes
b"25716f358c32750"
'foo'
The replacements are returned, along with the transformed code.
"""
replacements: list[Replacement] = []
try:
ast.parse(src)
except SyntaxError:
# Might have IPython magics, will process below.
pass
else:
# Syntax is fine, nothing to mask, early return.
return src, replacements
from IPython.core.inputtransformer2 import TransformerManager
transformer_manager = TransformerManager()
# A side effect of the following transformation is that it also removes any
# empty lines at the beginning of the cell.
transformed = transformer_manager.transform_cell(src)
transformed, cell_magic_replacements = replace_cell_magics(transformed)
replacements += cell_magic_replacements
transformed = transformer_manager.transform_cell(transformed)
transformed, magic_replacements = replace_magics(transformed)
if len(transformed.strip().splitlines()) != len(src.strip().splitlines()):
# Multi-line magic, not supported.
raise NothingChanged
replacements += magic_replacements
return transformed, replacements
def create_token(n_chars: int) -> str:
"""Create a randomly generated token that is n_chars characters long."""
assert n_chars > 0
if n_chars == 1:
return secrets.choice(string.ascii_letters)
if n_chars < 4:
return "_" + "".join(
secrets.choice(string.ascii_letters + string.digits + "_")
for _ in range(n_chars - 1)
)
n_bytes = max(n_chars // 2 - 1, 1)
token = secrets.token_hex(n_bytes)
if len(token) + 3 > n_chars:
token = token[:-1]
# We use a bytestring so that the string does not get interpreted
# as a docstring.
return f'b"{token}"'
def get_token(src: str, magic: str, existing_tokens: Collection[str] = ()) -> str:
"""Return randomly generated token to mask IPython magic with.
For example, if 'magic' was `%matplotlib inline`, then a possible
token to mask it with would be `"43fdd17f7e5ddc83"`. The token
will be the same length as the magic, and we make sure that it was
not already present anywhere else in the cell.
"""
assert magic
n_chars = len(magic)
token = create_token(n_chars)
counter = 0
while token in src or token in existing_tokens:
token = create_token(n_chars)
counter += 1
if counter > 100:
raise AssertionError(
"INTERNAL ERROR: Black was not able to replace IPython magic. "
"Please report a bug on https://github.com/psf/black/issues. "
f"The magic might be helpful: {magic}"
) from None
return token
def replace_cell_magics(src: str) -> tuple[str, list[Replacement]]:
r"""Replace cell magic with token.
Note that 'src' will already have been processed by IPython's
TransformerManager().transform_cell.
Example,
get_ipython().run_cell_magic('t', '-n1', 'ls =!ls\n')
becomes
"a794."
ls =!ls
The replacement, along with the transformed code, is returned.
"""
replacements: list[Replacement] = []
tree = ast.parse(src)
cell_magic_finder = CellMagicFinder()
cell_magic_finder.visit(tree)
if cell_magic_finder.cell_magic is None:
return src, replacements
header = cell_magic_finder.cell_magic.header
mask = get_token(src, header)
replacements.append(Replacement(mask=mask, src=header))
return f"{mask}\n{cell_magic_finder.cell_magic.body}", replacements
def replace_magics(src: str) -> tuple[str, list[Replacement]]:
"""Replace magics within body of cell.
Note that 'src' will already have been processed by IPython's
TransformerManager().transform_cell.
Example, this
get_ipython().run_line_magic('matplotlib', 'inline')
'foo'
becomes
"5e67db56d490fd39"
'foo'
The replacement, along with the transformed code, are returned.
"""
replacements = []
existing_tokens: set[str] = set()
magic_finder = MagicFinder()
magic_finder.visit(ast.parse(src))
new_srcs = []
for i, line in enumerate(src.split("\n"), start=1):
if i in magic_finder.magics:
offsets_and_magics = magic_finder.magics[i]
if len(offsets_and_magics) != 1: # pragma: nocover
raise AssertionError(
f"Expecting one magic per line, got: {offsets_and_magics}\n"
"Please report a bug on https://github.com/psf/black/issues."
)
col_offset, magic = (
offsets_and_magics[0].col_offset,
offsets_and_magics[0].magic,
)
mask = get_token(src, magic, existing_tokens)
replacements.append(Replacement(mask=mask, src=magic))
existing_tokens.add(mask)
line = line[:col_offset] + mask
new_srcs.append(line)
return "\n".join(new_srcs), replacements
def unmask_cell(src: str, replacements: list[Replacement]) -> str:
"""Remove replacements from cell.
For example
"9b20"
foo = bar
becomes
%%time
foo = bar
"""
for replacement in replacements:
if src.count(replacement.mask) != 1:
raise NothingChanged
src = src.replace(replacement.mask, replacement.src, 1)
return src
def _get_code_start(src: str) -> str:
"""Provides the first line where the code starts.
Iterates over lines of code until it finds the first line that doesn't
contain only empty spaces and comments. It removes any empty spaces at the
start of the line and returns it. If such line doesn't exist, it returns an
empty string.
"""
for match in re.finditer(".+", src):
line = match.group(0).lstrip()
if line and not line.startswith("#"):
return line
return ""
def _is_ipython_magic(node: ast.expr) -> TypeGuard[ast.Attribute]:
"""Check if attribute is IPython magic.
Note that the source of the abstract syntax tree
will already have been processed by IPython's
TransformerManager().transform_cell.
"""
return (
isinstance(node, ast.Attribute)
and isinstance(node.value, ast.Call)
and isinstance(node.value.func, ast.Name)
and node.value.func.id == "get_ipython"
)
def _get_str_args(args: list[ast.expr]) -> list[str]:
str_args = []
for arg in args:
assert isinstance(arg, ast.Constant) and isinstance(arg.value, str)
str_args.append(arg.value)
return str_args
@dataclasses.dataclass(frozen=True)
class CellMagic:
name: str
params: str | None
body: str
@property
def header(self) -> str:
if self.params:
return f"%%{self.name} {self.params}"
return f"%%{self.name}"
# ast.NodeVisitor + dataclass = breakage under mypyc.
class CellMagicFinder(ast.NodeVisitor):
r"""Find cell magics.
Note that the source of the abstract syntax tree
will already have been processed by IPython's
TransformerManager().transform_cell.
For example,
%%time\n
foo()
would have been transformed to
get_ipython().run_cell_magic('time', '', 'foo()\n')
and we look for instances of the latter.
"""
def __init__(self, cell_magic: CellMagic | None = None) -> None:
self.cell_magic = cell_magic
def visit_Expr(self, node: ast.Expr) -> None:
"""Find cell magic, extract header and body."""
if (
isinstance(node.value, ast.Call)
and _is_ipython_magic(node.value.func)
and node.value.func.attr == "run_cell_magic"
):
args = _get_str_args(node.value.args)
self.cell_magic = CellMagic(name=args[0], params=args[1], body=args[2])
self.generic_visit(node)
@dataclasses.dataclass(frozen=True)
class OffsetAndMagic:
col_offset: int
magic: str
# Unsurprisingly, subclassing ast.NodeVisitor means we can't use dataclasses here
# as mypyc will generate broken code.
class MagicFinder(ast.NodeVisitor):
"""Visit cell to look for get_ipython calls.
Note that the source of the abstract syntax tree
will already have been processed by IPython's
TransformerManager().transform_cell.
For example,
%matplotlib inline
would have been transformed to
get_ipython().run_line_magic('matplotlib', 'inline')
and we look for instances of the latter (and likewise for other
types of magics).
"""
def __init__(self) -> None:
self.magics: dict[int, list[OffsetAndMagic]] = collections.defaultdict(list)
def visit_Assign(self, node: ast.Assign) -> None:
"""Look for system assign magics.
For example,
black_version = !black --version
env = %env var
would have been (respectively) transformed to
black_version = get_ipython().getoutput('black --version')
env = get_ipython().run_line_magic('env', 'var')
and we look for instances of any of the latter.
"""
if isinstance(node.value, ast.Call) and _is_ipython_magic(node.value.func):
args = _get_str_args(node.value.args)
if node.value.func.attr == "getoutput":
src = f"!{args[0]}"
elif node.value.func.attr == "run_line_magic":
src = f"%{args[0]}"
if args[1]:
src += f" {args[1]}"
else:
raise AssertionError(
f"Unexpected IPython magic {node.value.func.attr!r} found. "
"Please report a bug on https://github.com/psf/black/issues."
) from None
self.magics[node.value.lineno].append(
OffsetAndMagic(node.value.col_offset, src)
)
self.generic_visit(node)
def visit_Expr(self, node: ast.Expr) -> None:
"""Look for magics in body of cell.
For examples,
!ls
!!ls
?ls
??ls
would (respectively) get transformed to
get_ipython().system('ls')
get_ipython().getoutput('ls')
get_ipython().run_line_magic('pinfo', 'ls')
get_ipython().run_line_magic('pinfo2', 'ls')
and we look for instances of any of the latter.
"""
if isinstance(node.value, ast.Call) and _is_ipython_magic(node.value.func):
args = _get_str_args(node.value.args)
if node.value.func.attr == "run_line_magic":
if args[0] == "pinfo":
src = f"?{args[1]}"
elif args[0] == "pinfo2":
src = f"??{args[1]}"
else:
src = f"%{args[0]}"
if args[1]:
src += f" {args[1]}"
elif node.value.func.attr == "system":
src = f"!{args[0]}"
elif node.value.func.attr == "getoutput":
src = f"!!{args[0]}"
else:
raise NothingChanged # unsupported magic.
self.magics[node.value.lineno].append(
OffsetAndMagic(node.value.col_offset, src)
)
self.generic_visit(node)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,321 @@
"""Data structures configuring Black behavior.
Mostly around Python language feature support per version and Black configuration
chosen by the user.
"""
from dataclasses import dataclass, field
from enum import Enum, auto
from hashlib import sha256
from operator import attrgetter
from typing import Final
from black.const import DEFAULT_LINE_LENGTH
class TargetVersion(Enum):
PY33 = 3
PY34 = 4
PY35 = 5
PY36 = 6
PY37 = 7
PY38 = 8
PY39 = 9
PY310 = 10
PY311 = 11
PY312 = 12
PY313 = 13
PY314 = 14
def pretty(self) -> str:
assert self.name[:2] == "PY"
return f"Python {self.name[2]}.{self.name[3:]}"
class Feature(Enum):
F_STRINGS = 2
NUMERIC_UNDERSCORES = 3
TRAILING_COMMA_IN_CALL = 4
TRAILING_COMMA_IN_DEF = 5
# The following two feature-flags are mutually exclusive, and exactly one should be
# set for every version of python.
ASYNC_IDENTIFIERS = 6
ASYNC_KEYWORDS = 7
ASSIGNMENT_EXPRESSIONS = 8
POS_ONLY_ARGUMENTS = 9
RELAXED_DECORATORS = 10
PATTERN_MATCHING = 11
UNPACKING_ON_FLOW = 12
ANN_ASSIGN_EXTENDED_RHS = 13
EXCEPT_STAR = 14
VARIADIC_GENERICS = 15
DEBUG_F_STRINGS = 16
PARENTHESIZED_CONTEXT_MANAGERS = 17
TYPE_PARAMS = 18
# FSTRING_PARSING = 19 # unused
TYPE_PARAM_DEFAULTS = 20
UNPARENTHESIZED_EXCEPT_TYPES = 21
T_STRINGS = 22
FORCE_OPTIONAL_PARENTHESES = 50
# __future__ flags
FUTURE_ANNOTATIONS = 51
FUTURE_FLAG_TO_FEATURE: Final = {
"annotations": Feature.FUTURE_ANNOTATIONS,
}
VERSION_TO_FEATURES: dict[TargetVersion, set[Feature]] = {
TargetVersion.PY33: {Feature.ASYNC_IDENTIFIERS},
TargetVersion.PY34: {Feature.ASYNC_IDENTIFIERS},
TargetVersion.PY35: {Feature.TRAILING_COMMA_IN_CALL, Feature.ASYNC_IDENTIFIERS},
TargetVersion.PY36: {
Feature.F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,
Feature.TRAILING_COMMA_IN_DEF,
Feature.ASYNC_IDENTIFIERS,
},
TargetVersion.PY37: {
Feature.F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,
Feature.TRAILING_COMMA_IN_DEF,
Feature.ASYNC_KEYWORDS,
Feature.FUTURE_ANNOTATIONS,
},
TargetVersion.PY38: {
Feature.F_STRINGS,
Feature.DEBUG_F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,
Feature.TRAILING_COMMA_IN_DEF,
Feature.ASYNC_KEYWORDS,
Feature.FUTURE_ANNOTATIONS,
Feature.ASSIGNMENT_EXPRESSIONS,
Feature.POS_ONLY_ARGUMENTS,
Feature.UNPACKING_ON_FLOW,
Feature.ANN_ASSIGN_EXTENDED_RHS,
},
TargetVersion.PY39: {
Feature.F_STRINGS,
Feature.DEBUG_F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,
Feature.TRAILING_COMMA_IN_DEF,
Feature.ASYNC_KEYWORDS,
Feature.FUTURE_ANNOTATIONS,
Feature.ASSIGNMENT_EXPRESSIONS,
Feature.RELAXED_DECORATORS,
Feature.POS_ONLY_ARGUMENTS,
Feature.UNPACKING_ON_FLOW,
Feature.ANN_ASSIGN_EXTENDED_RHS,
Feature.PARENTHESIZED_CONTEXT_MANAGERS,
},
TargetVersion.PY310: {
Feature.F_STRINGS,
Feature.DEBUG_F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,
Feature.TRAILING_COMMA_IN_DEF,
Feature.ASYNC_KEYWORDS,
Feature.FUTURE_ANNOTATIONS,
Feature.ASSIGNMENT_EXPRESSIONS,
Feature.RELAXED_DECORATORS,
Feature.POS_ONLY_ARGUMENTS,
Feature.UNPACKING_ON_FLOW,
Feature.ANN_ASSIGN_EXTENDED_RHS,
Feature.PARENTHESIZED_CONTEXT_MANAGERS,
Feature.PATTERN_MATCHING,
},
TargetVersion.PY311: {
Feature.F_STRINGS,
Feature.DEBUG_F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,
Feature.TRAILING_COMMA_IN_DEF,
Feature.ASYNC_KEYWORDS,
Feature.FUTURE_ANNOTATIONS,
Feature.ASSIGNMENT_EXPRESSIONS,
Feature.RELAXED_DECORATORS,
Feature.POS_ONLY_ARGUMENTS,
Feature.UNPACKING_ON_FLOW,
Feature.ANN_ASSIGN_EXTENDED_RHS,
Feature.PARENTHESIZED_CONTEXT_MANAGERS,
Feature.PATTERN_MATCHING,
Feature.EXCEPT_STAR,
Feature.VARIADIC_GENERICS,
},
TargetVersion.PY312: {
Feature.F_STRINGS,
Feature.DEBUG_F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,
Feature.TRAILING_COMMA_IN_DEF,
Feature.ASYNC_KEYWORDS,
Feature.FUTURE_ANNOTATIONS,
Feature.ASSIGNMENT_EXPRESSIONS,
Feature.RELAXED_DECORATORS,
Feature.POS_ONLY_ARGUMENTS,
Feature.UNPACKING_ON_FLOW,
Feature.ANN_ASSIGN_EXTENDED_RHS,
Feature.PARENTHESIZED_CONTEXT_MANAGERS,
Feature.PATTERN_MATCHING,
Feature.EXCEPT_STAR,
Feature.VARIADIC_GENERICS,
Feature.TYPE_PARAMS,
},
TargetVersion.PY313: {
Feature.F_STRINGS,
Feature.DEBUG_F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,
Feature.TRAILING_COMMA_IN_DEF,
Feature.ASYNC_KEYWORDS,
Feature.FUTURE_ANNOTATIONS,
Feature.ASSIGNMENT_EXPRESSIONS,
Feature.RELAXED_DECORATORS,
Feature.POS_ONLY_ARGUMENTS,
Feature.UNPACKING_ON_FLOW,
Feature.ANN_ASSIGN_EXTENDED_RHS,
Feature.PARENTHESIZED_CONTEXT_MANAGERS,
Feature.PATTERN_MATCHING,
Feature.EXCEPT_STAR,
Feature.VARIADIC_GENERICS,
Feature.TYPE_PARAMS,
Feature.TYPE_PARAM_DEFAULTS,
},
TargetVersion.PY314: {
Feature.F_STRINGS,
Feature.DEBUG_F_STRINGS,
Feature.NUMERIC_UNDERSCORES,
Feature.TRAILING_COMMA_IN_CALL,
Feature.TRAILING_COMMA_IN_DEF,
Feature.ASYNC_KEYWORDS,
Feature.FUTURE_ANNOTATIONS,
Feature.ASSIGNMENT_EXPRESSIONS,
Feature.RELAXED_DECORATORS,
Feature.POS_ONLY_ARGUMENTS,
Feature.UNPACKING_ON_FLOW,
Feature.ANN_ASSIGN_EXTENDED_RHS,
Feature.PARENTHESIZED_CONTEXT_MANAGERS,
Feature.PATTERN_MATCHING,
Feature.EXCEPT_STAR,
Feature.VARIADIC_GENERICS,
Feature.TYPE_PARAMS,
Feature.TYPE_PARAM_DEFAULTS,
Feature.UNPARENTHESIZED_EXCEPT_TYPES,
Feature.T_STRINGS,
},
}
def supports_feature(target_versions: set[TargetVersion], feature: Feature) -> bool:
if not target_versions:
raise ValueError("At least one target Python version must be specified.")
return all(feature in VERSION_TO_FEATURES[version] for version in target_versions)
class Preview(Enum):
"""Individual preview style features."""
# NOTE: string_processing requires wrap_long_dict_values_in_parens
# for https://github.com/psf/black/issues/3117 to be fixed.
string_processing = auto()
hug_parens_with_braces_and_square_brackets = auto()
wrap_comprehension_in = auto()
simplify_power_operator_hugging = auto()
wrap_long_dict_values_in_parens = auto()
fix_if_guard_explosion_in_case_statement = auto()
UNSTABLE_FEATURES: set[Preview] = {
# Many issues, see summary in https://github.com/psf/black/issues/4208
Preview.string_processing,
# See issue #4036 (crash), #4098, #4099 (proposed tweaks)
Preview.hug_parens_with_braces_and_square_brackets,
}
_MAX_CACHE_KEY_PART_LENGTH: Final = 32
@dataclass
class Mode:
target_versions: set[TargetVersion] = field(default_factory=set)
line_length: int = DEFAULT_LINE_LENGTH
string_normalization: bool = True
is_pyi: bool = False
is_ipynb: bool = False
skip_source_first_line: bool = False
magic_trailing_comma: bool = True
python_cell_magics: set[str] = field(default_factory=set)
preview: bool = False
unstable: bool = False
enabled_features: set[Preview] = field(default_factory=set)
def __contains__(self, feature: Preview) -> bool:
"""
Provide `Preview.FEATURE in Mode` syntax that mirrors the ``preview`` flag.
In unstable mode, all features are enabled. In preview mode, all features
except those in UNSTABLE_FEATURES are enabled. Any features in
`self.enabled_features` are also enabled.
"""
if self.unstable:
return True
if feature in self.enabled_features:
return True
return self.preview and feature not in UNSTABLE_FEATURES
def get_cache_key(self) -> str:
if self.target_versions:
version_str = ",".join(
str(version.value)
for version in sorted(self.target_versions, key=attrgetter("value"))
)
else:
version_str = "-"
if len(version_str) > _MAX_CACHE_KEY_PART_LENGTH:
version_str = sha256(version_str.encode()).hexdigest()[
:_MAX_CACHE_KEY_PART_LENGTH
]
features_and_magics = (
",".join(sorted(f.name for f in self.enabled_features))
+ "@"
+ ",".join(sorted(self.python_cell_magics))
)
features_and_magics = sha256(features_and_magics.encode()).hexdigest()[
:_MAX_CACHE_KEY_PART_LENGTH
]
parts = [
version_str,
str(self.line_length),
str(int(self.string_normalization)),
str(int(self.is_pyi)),
str(int(self.is_ipynb)),
str(int(self.skip_source_first_line)),
str(int(self.magic_trailing_comma)),
str(int(self.preview)),
str(int(self.unstable)),
features_and_magics,
]
return ".".join(parts)
def __hash__(self) -> int:
return hash((
frozenset(self.target_versions),
self.line_length,
self.string_normalization,
self.is_pyi,
self.is_ipynb,
self.skip_source_first_line,
self.magic_trailing_comma,
frozenset(self.python_cell_magics),
self.preview,
self.unstable,
frozenset(self.enabled_features),
))

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,61 @@
"""
Formatting numeric literals.
"""
from blib2to3.pytree import Leaf
def format_hex(text: str) -> str:
"""
Formats a hexadecimal string like "0x12B3"
"""
before, after = text[:2], text[2:]
return f"{before}{after.upper()}"
def format_scientific_notation(text: str) -> str:
"""Formats a numeric string utilizing scientific notation"""
before, after = text.split("e")
sign = ""
if after.startswith("-"):
after = after[1:]
sign = "-"
elif after.startswith("+"):
after = after[1:]
before = format_float_or_int_string(before)
return f"{before}e{sign}{after}"
def format_complex_number(text: str) -> str:
"""Formats a complex string like `10j`"""
number = text[:-1]
suffix = text[-1]
return f"{format_float_or_int_string(number)}{suffix}"
def format_float_or_int_string(text: str) -> str:
"""Formats a float string like "1.0"."""
if "." not in text:
return text
before, after = text.split(".")
return f"{before or 0}.{after or 0}"
def normalize_numeric_literal(leaf: Leaf) -> None:
"""Normalizes numeric (float, int, and complex) literals.
All letters used in the representation are normalized to lowercase."""
text = leaf.value.lower()
if text.startswith(("0o", "0b")):
# Leave octal and binary literals alone.
pass
elif text.startswith("0x"):
text = format_hex(text)
elif "e" in text:
text = format_scientific_notation(text)
elif text.endswith("j"):
text = format_complex_number(text)
else:
text = format_float_or_int_string(text)
leaf.value = text

View file

@ -0,0 +1,122 @@
"""Nice output for Black.
The double calls are for patching purposes in tests.
"""
import json
import re
import tempfile
from typing import Any
from click import echo, style
from mypy_extensions import mypyc_attr
@mypyc_attr(patchable=True)
def _out(message: str | None = None, nl: bool = True, **styles: Any) -> None:
if message is not None:
if "bold" not in styles:
styles["bold"] = True
message = style(message, **styles)
echo(message, nl=nl, err=True)
@mypyc_attr(patchable=True)
def _err(message: str | None = None, nl: bool = True, **styles: Any) -> None:
if message is not None:
if "fg" not in styles:
styles["fg"] = "red"
message = style(message, **styles)
echo(message, nl=nl, err=True)
@mypyc_attr(patchable=True)
def out(message: str | None = None, nl: bool = True, **styles: Any) -> None:
_out(message, nl=nl, **styles)
def err(message: str | None = None, nl: bool = True, **styles: Any) -> None:
_err(message, nl=nl, **styles)
def ipynb_diff(a: str, b: str, a_name: str, b_name: str) -> str:
"""Return a unified diff string between each cell in notebooks `a` and `b`."""
a_nb = json.loads(a)
b_nb = json.loads(b)
diff_lines = [
diff(
"".join(a_nb["cells"][cell_number]["source"]) + "\n",
"".join(b_nb["cells"][cell_number]["source"]) + "\n",
f"{a_name}:cell_{cell_number}",
f"{b_name}:cell_{cell_number}",
)
for cell_number, cell in enumerate(a_nb["cells"])
if cell["cell_type"] == "code"
]
return "".join(diff_lines)
_line_pattern = re.compile(r"(.*?(?:\r\n|\n|\r|$))")
def _splitlines_no_ff(source: str) -> list[str]:
"""Split a string into lines ignoring form feed and other chars.
This mimics how the Python parser splits source code.
A simplified version of the function with the same name in Lib/ast.py
"""
result = [match[0] for match in _line_pattern.finditer(source)]
if result[-1] == "":
result.pop(-1)
return result
def diff(a: str, b: str, a_name: str, b_name: str) -> str:
"""Return a unified diff string between strings `a` and `b`."""
import difflib
a_lines = _splitlines_no_ff(a)
b_lines = _splitlines_no_ff(b)
diff_lines = []
for line in difflib.unified_diff(
a_lines, b_lines, fromfile=a_name, tofile=b_name, n=5
):
# Work around https://bugs.python.org/issue2142
# See:
# https://www.gnu.org/software/diffutils/manual/html_node/Incomplete-Lines.html
if line[-1] == "\n":
diff_lines.append(line)
else:
diff_lines.append(line + "\n")
diff_lines.append("\\ No newline at end of file\n")
return "".join(diff_lines)
def color_diff(contents: str) -> str:
"""Inject the ANSI color codes to the diff."""
lines = contents.split("\n")
for i, line in enumerate(lines):
if line.startswith("+++") or line.startswith("---"):
line = "\033[1m" + line + "\033[0m" # bold, reset
elif line.startswith("@@"):
line = "\033[36m" + line + "\033[0m" # cyan, reset
elif line.startswith("+"):
line = "\033[32m" + line + "\033[0m" # green, reset
elif line.startswith("-"):
line = "\033[31m" + line + "\033[0m" # red, reset
lines[i] = line
return "\n".join(lines)
@mypyc_attr(patchable=True)
def dump_to_file(*output: str, ensure_final_newline: bool = True) -> str:
"""Dump `output` to a temporary file. Return path to the file."""
with tempfile.NamedTemporaryFile(
mode="w", prefix="blk_", suffix=".log", delete=False, encoding="utf8"
) as f:
for lines in output:
f.write(lines)
if ensure_final_newline and lines and lines[-1] != "\n":
f.write("\n")
return f.name

View file

@ -0,0 +1,244 @@
"""
Parse Python code and perform AST validation.
"""
import ast
import sys
import warnings
from collections.abc import Collection, Iterator
from black.mode import VERSION_TO_FEATURES, Feature, TargetVersion, supports_feature
from black.nodes import syms
from blib2to3 import pygram
from blib2to3.pgen2 import driver
from blib2to3.pgen2.grammar import Grammar
from blib2to3.pgen2.parse import ParseError
from blib2to3.pgen2.tokenize import TokenError
from blib2to3.pytree import Leaf, Node
class InvalidInput(ValueError):
"""Raised when input source code fails all parse attempts."""
def get_grammars(target_versions: set[TargetVersion]) -> list[Grammar]:
if not target_versions:
# No target_version specified, so try all grammars.
return [
# Python 3.7-3.9
pygram.python_grammar_async_keywords,
# Python 3.0-3.6
pygram.python_grammar,
# Python 3.10+
pygram.python_grammar_soft_keywords,
]
grammars = []
# If we have to parse both, try to parse async as a keyword first
if not supports_feature(
target_versions, Feature.ASYNC_IDENTIFIERS
) and not supports_feature(target_versions, Feature.PATTERN_MATCHING):
# Python 3.7-3.9
grammars.append(pygram.python_grammar_async_keywords)
if not supports_feature(target_versions, Feature.ASYNC_KEYWORDS):
# Python 3.0-3.6
grammars.append(pygram.python_grammar)
if any(Feature.PATTERN_MATCHING in VERSION_TO_FEATURES[v] for v in target_versions):
# Python 3.10+
grammars.append(pygram.python_grammar_soft_keywords)
# At least one of the above branches must have been taken, because every Python
# version has exactly one of the two 'ASYNC_*' flags
return grammars
def lib2to3_parse(
src_txt: str, target_versions: Collection[TargetVersion] = ()
) -> Node:
"""Given a string with source, return the lib2to3 Node."""
if not src_txt.endswith("\n"):
src_txt += "\n"
grammars = get_grammars(set(target_versions))
if target_versions:
max_tv = max(target_versions, key=lambda tv: tv.value)
tv_str = f" for target version {max_tv.pretty()}"
else:
tv_str = ""
errors = {}
for grammar in grammars:
drv = driver.Driver(grammar)
try:
result = drv.parse_string(src_txt, False)
break
except ParseError as pe:
lineno, column = pe.context[1]
lines = src_txt.splitlines()
try:
faulty_line = lines[lineno - 1]
except IndexError:
faulty_line = "<line number missing in source>"
errors[grammar.version] = InvalidInput(
f"Cannot parse{tv_str}: {lineno}:{column}: {faulty_line}"
)
except TokenError as te:
# In edge cases these are raised; and typically don't have a "faulty_line".
lineno, column = te.args[1]
errors[grammar.version] = InvalidInput(
f"Cannot parse{tv_str}: {lineno}:{column}: {te.args[0]}"
)
else:
# Choose the latest version when raising the actual parsing error.
assert len(errors) >= 1
exc = errors[max(errors)]
raise exc from None
if isinstance(result, Leaf):
result = Node(syms.file_input, [result])
return result
class ASTSafetyError(Exception):
"""Raised when Black's generated code is not equivalent to the old AST."""
def _parse_single_version(
src: str, version: tuple[int, int], *, type_comments: bool
) -> ast.AST:
filename = "<unknown>"
with warnings.catch_warnings():
warnings.simplefilter("ignore", SyntaxWarning)
warnings.simplefilter("ignore", DeprecationWarning)
return ast.parse(
src, filename, feature_version=version, type_comments=type_comments
)
def parse_ast(src: str) -> ast.AST:
# TODO: support Python 4+ ;)
versions = [(3, minor) for minor in range(3, sys.version_info[1] + 1)]
first_error = ""
for version in sorted(versions, reverse=True):
try:
return _parse_single_version(src, version, type_comments=True)
except SyntaxError as e:
if not first_error:
first_error = str(e)
# Try to parse without type comments
for version in sorted(versions, reverse=True):
try:
return _parse_single_version(src, version, type_comments=False)
except SyntaxError:
pass
raise SyntaxError(first_error)
def _normalize(lineend: str, value: str) -> str:
# To normalize, we strip any leading and trailing space from
# each line...
stripped: list[str] = [i.strip() for i in value.splitlines()]
normalized = lineend.join(stripped)
# ...and remove any blank lines at the beginning and end of
# the whole string
return normalized.strip()
def stringify_ast(node: ast.AST) -> Iterator[str]:
"""Simple visitor generating strings to compare ASTs by content."""
return _stringify_ast(node, [])
def _stringify_ast_with_new_parent(
node: ast.AST, parent_stack: list[ast.AST], new_parent: ast.AST
) -> Iterator[str]:
parent_stack.append(new_parent)
yield from _stringify_ast(node, parent_stack)
parent_stack.pop()
def _stringify_ast(node: ast.AST, parent_stack: list[ast.AST]) -> Iterator[str]:
if (
isinstance(node, ast.Constant)
and isinstance(node.value, str)
and node.kind == "u"
):
# It's a quirk of history that we strip the u prefix over here. We used to
# rewrite the AST nodes for Python version compatibility and we never copied
# over the kind
node.kind = None
yield f"{' ' * len(parent_stack)}{node.__class__.__name__}("
for field in sorted(node._fields):
# TypeIgnore has only one field 'lineno' which breaks this comparison
if isinstance(node, ast.TypeIgnore):
break
try:
value: object = getattr(node, field)
except AttributeError:
continue
yield f"{' ' * (len(parent_stack) + 1)}{field}="
if isinstance(value, list):
for item in value:
# Ignore nested tuples within del statements, because we may insert
# parentheses and they change the AST.
if (
field == "targets"
and isinstance(node, ast.Delete)
and isinstance(item, ast.Tuple)
):
for elt in _unwrap_tuples(item):
yield from _stringify_ast_with_new_parent(
elt, parent_stack, node
)
elif isinstance(item, ast.AST):
yield from _stringify_ast_with_new_parent(item, parent_stack, node)
elif isinstance(value, ast.AST):
yield from _stringify_ast_with_new_parent(value, parent_stack, node)
else:
normalized: object
if (
isinstance(node, ast.Constant)
and field == "value"
and isinstance(value, str)
and len(parent_stack) >= 2
# Any standalone string, ideally this would
# exactly match black.nodes.is_docstring
and isinstance(parent_stack[-1], ast.Expr)
):
# Constant strings may be indented across newlines, if they are
# docstrings; fold spaces after newlines when comparing. Similarly,
# trailing and leading space may be removed.
normalized = _normalize("\n", value)
elif field == "type_comment" and isinstance(value, str):
# Trailing whitespace in type comments is removed.
normalized = value.rstrip()
else:
normalized = value
yield (
f"{' ' * (len(parent_stack) + 1)}{normalized!r}, #"
f" {value.__class__.__name__}"
)
yield f"{' ' * len(parent_stack)}) # /{node.__class__.__name__}"
def _unwrap_tuples(node: ast.Tuple) -> Iterator[ast.AST]:
for elt in node.elts:
if isinstance(elt, ast.Tuple):
yield from _unwrap_tuples(elt)
else:
yield elt

View file

@ -0,0 +1,534 @@
"""Functions related to Black's formatting by line ranges feature."""
import difflib
from collections.abc import Collection, Iterator, Sequence
from dataclasses import dataclass
from black.nodes import (
LN,
STANDALONE_COMMENT,
Leaf,
Node,
Visitor,
first_leaf,
furthest_ancestor_with_last_leaf,
last_leaf,
syms,
)
from blib2to3.pgen2.token import ASYNC, NEWLINE
def parse_line_ranges(line_ranges: Sequence[str]) -> list[tuple[int, int]]:
lines: list[tuple[int, int]] = []
for lines_str in line_ranges:
parts = lines_str.split("-")
if len(parts) != 2:
raise ValueError(
"Incorrect --line-ranges format, expect 'START-END', found"
f" {lines_str!r}"
)
try:
start = int(parts[0])
end = int(parts[1])
except ValueError:
raise ValueError(
"Incorrect --line-ranges value, expect integer ranges, found"
f" {lines_str!r}"
) from None
else:
lines.append((start, end))
return lines
def is_valid_line_range(lines: tuple[int, int]) -> bool:
"""Returns whether the line range is valid."""
return not lines or lines[0] <= lines[1]
def sanitized_lines(
lines: Collection[tuple[int, int]], src_contents: str
) -> Collection[tuple[int, int]]:
"""Returns the valid line ranges for the given source.
This removes ranges that are entirely outside the valid lines.
Other ranges are normalized so that the start values are at least 1 and the
end values are at most the (1-based) index of the last source line.
"""
if not src_contents:
return []
good_lines = []
src_line_count = src_contents.count("\n")
if not src_contents.endswith("\n"):
src_line_count += 1
for start, end in lines:
if start > src_line_count:
continue
# line-ranges are 1-based
start = max(start, 1)
if end < start:
continue
end = min(end, src_line_count)
good_lines.append((start, end))
return good_lines
def adjusted_lines(
lines: Collection[tuple[int, int]],
original_source: str,
modified_source: str,
) -> list[tuple[int, int]]:
"""Returns the adjusted line ranges based on edits from the original code.
This computes the new line ranges by diffing original_source and
modified_source, and adjust each range based on how the range overlaps with
the diffs.
Note the diff can contain lines outside of the original line ranges. This can
happen when the formatting has to be done in adjacent to maintain consistent
local results. For example:
1. def my_func(arg1, arg2,
2. arg3,):
3. pass
If it restricts to line 2-2, it can't simply reformat line 2, it also has
to reformat line 1:
1. def my_func(
2. arg1,
3. arg2,
4. arg3,
5. ):
6. pass
In this case, we will expand the line ranges to also include the whole diff
block.
Args:
lines: a collection of line ranges.
original_source: the original source.
modified_source: the modified source.
"""
lines_mappings = _calculate_lines_mappings(original_source, modified_source)
new_lines = []
# Keep an index of the current search. Since the lines and lines_mappings are
# sorted, this makes the search complexity linear.
current_mapping_index = 0
for start, end in sorted(lines):
start_mapping_index = _find_lines_mapping_index(
start,
lines_mappings,
current_mapping_index,
)
end_mapping_index = _find_lines_mapping_index(
end,
lines_mappings,
start_mapping_index,
)
current_mapping_index = start_mapping_index
if start_mapping_index >= len(lines_mappings) or end_mapping_index >= len(
lines_mappings
):
# Protect against invalid inputs.
continue
start_mapping = lines_mappings[start_mapping_index]
end_mapping = lines_mappings[end_mapping_index]
if start_mapping.is_changed_block:
# When the line falls into a changed block, expands to the whole block.
new_start = start_mapping.modified_start
else:
new_start = (
start - start_mapping.original_start + start_mapping.modified_start
)
if end_mapping.is_changed_block:
# When the line falls into a changed block, expands to the whole block.
new_end = end_mapping.modified_end
else:
new_end = end - end_mapping.original_start + end_mapping.modified_start
new_range = (new_start, new_end)
if is_valid_line_range(new_range):
new_lines.append(new_range)
return new_lines
def convert_unchanged_lines(src_node: Node, lines: Collection[tuple[int, int]]) -> None:
r"""Converts unchanged lines to STANDALONE_COMMENT.
The idea is similar to how `# fmt: on/off` is implemented. It also converts the
nodes between those markers as a single `STANDALONE_COMMENT` leaf node with
the unformatted code as its value. `STANDALONE_COMMENT` is a "fake" token
that will be formatted as-is with its prefix normalized.
Here we perform two passes:
1. Visit the top-level statements, and convert them to a single
`STANDALONE_COMMENT` when unchanged. This speeds up formatting when some
of the top-level statements aren't changed.
2. Convert unchanged "unwrapped lines" to `STANDALONE_COMMENT` nodes line by
line. "unwrapped lines" are divided by the `NEWLINE` token. e.g. a
multi-line statement is *one* "unwrapped line" that ends with `NEWLINE`,
even though this statement itself can span multiple lines, and the
tokenizer only sees the last '\n' as the `NEWLINE` token.
NOTE: During pass (2), comment prefixes and indentations are ALWAYS
normalized even when the lines aren't changed. This is fixable by moving
more formatting to pass (1). However, it's hard to get it correct when
incorrect indentations are used. So we defer this to future optimizations.
"""
lines_set: set[int] = set()
for start, end in lines:
lines_set.update(range(start, end + 1))
visitor = _TopLevelStatementsVisitor(lines_set)
_ = list(visitor.visit(src_node)) # Consume all results.
_convert_unchanged_line_by_line(src_node, lines_set)
def _contains_standalone_comment(node: LN) -> bool:
if isinstance(node, Leaf):
return node.type == STANDALONE_COMMENT
else:
for child in node.children:
if _contains_standalone_comment(child):
return True
return False
class _TopLevelStatementsVisitor(Visitor[None]):
"""
A node visitor that converts unchanged top-level statements to
STANDALONE_COMMENT.
This is used in addition to _convert_unchanged_line_by_line, to
speed up formatting when there are unchanged top-level
classes/functions/statements.
"""
def __init__(self, lines_set: set[int]):
self._lines_set = lines_set
def visit_simple_stmt(self, node: Node) -> Iterator[None]:
# This is only called for top-level statements, since `visit_suite`
# won't visit its children nodes.
yield from []
newline_leaf = last_leaf(node)
if not newline_leaf:
return
assert (
newline_leaf.type == NEWLINE
), f"Unexpectedly found leaf.type={newline_leaf.type}"
# We need to find the furthest ancestor with the NEWLINE as the last
# leaf, since a `suite` can simply be a `simple_stmt` when it puts
# its body on the same line. Example: `if cond: pass`.
ancestor = furthest_ancestor_with_last_leaf(newline_leaf)
if not _get_line_range(ancestor).intersection(self._lines_set):
_convert_node_to_standalone_comment(ancestor)
def visit_suite(self, node: Node) -> Iterator[None]:
yield from []
# If there is a STANDALONE_COMMENT node, it means parts of the node tree
# have fmt on/off/skip markers. Those STANDALONE_COMMENT nodes can't
# be simply converted by calling str(node). So we just don't convert
# here.
if _contains_standalone_comment(node):
return
# Find the semantic parent of this suite. For `async_stmt` and
# `async_funcdef`, the ASYNC token is defined on a separate level by the
# grammar.
semantic_parent = node.parent
if semantic_parent is not None:
if (
semantic_parent.prev_sibling is not None
and semantic_parent.prev_sibling.type == ASYNC
):
semantic_parent = semantic_parent.parent
if semantic_parent is not None and not _get_line_range(
semantic_parent
).intersection(self._lines_set):
_convert_node_to_standalone_comment(semantic_parent)
def _convert_unchanged_line_by_line(node: Node, lines_set: set[int]) -> None:
"""Converts unchanged to STANDALONE_COMMENT line by line."""
for leaf in node.leaves():
if leaf.type != NEWLINE:
# We only consider "unwrapped lines", which are divided by the NEWLINE
# token.
continue
if leaf.parent and leaf.parent.type == syms.match_stmt:
# The `suite` node is defined as:
# match_stmt: "match" subject_expr ':' NEWLINE INDENT case_block+ DEDENT
# Here we need to check `subject_expr`. The `case_block+` will be
# checked by their own NEWLINEs.
nodes_to_ignore: list[LN] = []
prev_sibling = leaf.prev_sibling
while prev_sibling:
nodes_to_ignore.insert(0, prev_sibling)
prev_sibling = prev_sibling.prev_sibling
if not _get_line_range(nodes_to_ignore).intersection(lines_set):
_convert_nodes_to_standalone_comment(nodes_to_ignore, newline=leaf)
elif leaf.parent and leaf.parent.type == syms.suite:
# The `suite` node is defined as:
# suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT
# We will check `simple_stmt` and `stmt+` separately against the lines set
parent_sibling = leaf.parent.prev_sibling
nodes_to_ignore = []
while parent_sibling and parent_sibling.type != syms.suite:
# NOTE: Multiple suite nodes can exist as siblings in e.g. `if_stmt`.
nodes_to_ignore.insert(0, parent_sibling)
parent_sibling = parent_sibling.prev_sibling
# Special case for `async_stmt` and `async_funcdef` where the ASYNC
# token is on the grandparent node.
grandparent = leaf.parent.parent
if (
grandparent is not None
and grandparent.prev_sibling is not None
and grandparent.prev_sibling.type == ASYNC
):
nodes_to_ignore.insert(0, grandparent.prev_sibling)
if not _get_line_range(nodes_to_ignore).intersection(lines_set):
_convert_nodes_to_standalone_comment(nodes_to_ignore, newline=leaf)
else:
ancestor = furthest_ancestor_with_last_leaf(leaf)
# Consider multiple decorators as a whole block, as their
# newlines have different behaviors than the rest of the grammar.
if (
ancestor.type == syms.decorator
and ancestor.parent
and ancestor.parent.type == syms.decorators
):
ancestor = ancestor.parent
if not _get_line_range(ancestor).intersection(lines_set):
_convert_node_to_standalone_comment(ancestor)
def _convert_node_to_standalone_comment(node: LN) -> None:
"""Convert node to STANDALONE_COMMENT by modifying the tree inline."""
parent = node.parent
if not parent:
return
first = first_leaf(node)
last = last_leaf(node)
if not first or not last:
return
if first is last:
# This can happen on the following edge cases:
# 1. A block of `# fmt: off/on` code except the `# fmt: on` is placed
# on the end of the last line instead of on a new line.
# 2. A single backslash on its own line followed by a comment line.
# Ideally we don't want to format them when not requested, but fixing
# isn't easy. These cases are also badly formatted code, so it isn't
# too bad we reformat them.
return
# The prefix contains comments and indentation whitespaces. They are
# reformatted accordingly to the correct indentation level.
# This also means the indentation will be changed on the unchanged lines, and
# this is actually required to not break incremental reformatting.
prefix = first.prefix
first.prefix = ""
index = node.remove()
if index is not None:
# Because of the special handling of multiple decorators, if the decorated
# item is a single line then there will be a missing newline between the
# decorator and item, so add it back. This doesn't affect any other case
# since a decorated item with a newline would hit the earlier suite case
# in _convert_unchanged_line_by_line that correctly handles the newlines.
if node.type == syms.decorated:
# A leaf of type decorated wouldn't make sense, since it should always
# have at least the decorator + the decorated item, so if this assert
# hits that means there's a problem in the parser.
assert isinstance(node, Node)
# 1 will always be the correct index since before this function is
# called all the decorators are collapsed into a single leaf
node.insert_child(1, Leaf(NEWLINE, "\n"))
# Remove the '\n', as STANDALONE_COMMENT will have '\n' appended when
# generating the formatted code.
value = str(node)[:-1]
parent.insert_child(
index,
Leaf(
STANDALONE_COMMENT,
value,
prefix=prefix,
fmt_pass_converted_first_leaf=first,
),
)
def _convert_nodes_to_standalone_comment(nodes: Sequence[LN], *, newline: Leaf) -> None:
"""Convert nodes to STANDALONE_COMMENT by modifying the tree inline."""
if not nodes:
return
parent = nodes[0].parent
first = first_leaf(nodes[0])
if not parent or not first:
return
prefix = first.prefix
first.prefix = ""
value = "".join(str(node) for node in nodes)
# The prefix comment on the NEWLINE leaf is the trailing comment of the statement.
if newline.prefix:
value += newline.prefix
newline.prefix = ""
index = nodes[0].remove()
for node in nodes[1:]:
node.remove()
if index is not None:
parent.insert_child(
index,
Leaf(
STANDALONE_COMMENT,
value,
prefix=prefix,
fmt_pass_converted_first_leaf=first,
),
)
def _leaf_line_end(leaf: Leaf) -> int:
"""Returns the line number of the leaf node's last line."""
if leaf.type == NEWLINE:
return leaf.lineno
else:
# Leaf nodes like multiline strings can occupy multiple lines.
return leaf.lineno + str(leaf).count("\n")
def _get_line_range(node_or_nodes: LN | list[LN]) -> set[int]:
"""Returns the line range of this node or list of nodes."""
if isinstance(node_or_nodes, list):
nodes = node_or_nodes
if not nodes:
return set()
first = first_leaf(nodes[0])
last = last_leaf(nodes[-1])
if first and last:
line_start = first.lineno
line_end = _leaf_line_end(last)
return set(range(line_start, line_end + 1))
else:
return set()
else:
node = node_or_nodes
if isinstance(node, Leaf):
return set(range(node.lineno, _leaf_line_end(node) + 1))
else:
first = first_leaf(node)
last = last_leaf(node)
if first and last:
return set(range(first.lineno, _leaf_line_end(last) + 1))
else:
return set()
@dataclass
class _LinesMapping:
"""1-based lines mapping from original source to modified source.
Lines [original_start, original_end] from original source
are mapped to [modified_start, modified_end].
The ranges are inclusive on both ends.
"""
original_start: int
original_end: int
modified_start: int
modified_end: int
# Whether this range corresponds to a changed block, or an unchanged block.
is_changed_block: bool
def _calculate_lines_mappings(
original_source: str,
modified_source: str,
) -> Sequence[_LinesMapping]:
"""Returns a sequence of _LinesMapping by diffing the sources.
For example, given the following diff:
import re
- def func(arg1,
- arg2, arg3):
+ def func(arg1, arg2, arg3):
pass
It returns the following mappings:
original -> modified
(1, 1) -> (1, 1), is_changed_block=False (the "import re" line)
(2, 3) -> (2, 2), is_changed_block=True (the diff)
(4, 4) -> (3, 3), is_changed_block=False (the "pass" line)
You can think of this visually as if it brings up a side-by-side diff, and tries
to map the line ranges from the left side to the right side:
(1, 1)->(1, 1) 1. import re 1. import re
(2, 3)->(2, 2) 2. def func(arg1, 2. def func(arg1, arg2, arg3):
3. arg2, arg3):
(4, 4)->(3, 3) 4. pass 3. pass
Args:
original_source: the original source.
modified_source: the modified source.
"""
matcher = difflib.SequenceMatcher(
None,
original_source.splitlines(keepends=True),
modified_source.splitlines(keepends=True),
)
matching_blocks = matcher.get_matching_blocks()
lines_mappings: list[_LinesMapping] = []
# matching_blocks is a sequence of "same block of code ranges", see
# https://docs.python.org/3/library/difflib.html#difflib.SequenceMatcher.get_matching_blocks
# Each block corresponds to a _LinesMapping with is_changed_block=False,
# and the ranges between two blocks corresponds to a _LinesMapping with
# is_changed_block=True,
# NOTE: matching_blocks is 0-based, but _LinesMapping is 1-based.
for i, block in enumerate(matching_blocks):
if i == 0:
if block.a != 0 or block.b != 0:
lines_mappings.append(
_LinesMapping(
original_start=1,
original_end=block.a,
modified_start=1,
modified_end=block.b,
is_changed_block=False,
)
)
else:
previous_block = matching_blocks[i - 1]
lines_mappings.append(
_LinesMapping(
original_start=previous_block.a + previous_block.size + 1,
original_end=block.a,
modified_start=previous_block.b + previous_block.size + 1,
modified_end=block.b,
is_changed_block=True,
)
)
if i < len(matching_blocks) - 1:
lines_mappings.append(
_LinesMapping(
original_start=block.a + 1,
original_end=block.a + block.size,
modified_start=block.b + 1,
modified_end=block.b + block.size,
is_changed_block=False,
)
)
return lines_mappings
def _find_lines_mapping_index(
original_line: int,
lines_mappings: Sequence[_LinesMapping],
start_index: int,
) -> int:
"""Returns the original index of the lines mappings for the original line."""
index = start_index
while index < len(lines_mappings):
mapping = lines_mappings[index]
if mapping.original_start <= original_line <= mapping.original_end:
return index
index += 1
return index

View file

@ -0,0 +1,107 @@
"""
Summarize Black runs to users.
"""
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from click import style
from black.output import err, out
class Changed(Enum):
NO = 0
CACHED = 1
YES = 2
class NothingChanged(UserWarning):
"""Raised when reformatted code is the same as source."""
@dataclass
class Report:
"""Provides a reformatting counter. Can be rendered with `str(report)`."""
check: bool = False
diff: bool = False
quiet: bool = False
verbose: bool = False
change_count: int = 0
same_count: int = 0
failure_count: int = 0
def done(self, src: Path, changed: Changed) -> None:
"""Increment the counter for successful reformatting. Write out a message."""
if changed is Changed.YES:
reformatted = "would reformat" if self.check or self.diff else "reformatted"
if self.verbose or not self.quiet:
out(f"{reformatted} {src}")
self.change_count += 1
else:
if self.verbose:
if changed is Changed.NO:
msg = f"{src} already well formatted, good job."
else:
msg = f"{src} wasn't modified on disk since last run."
out(msg, bold=False)
self.same_count += 1
def failed(self, src: Path, message: str) -> None:
"""Increment the counter for failed reformatting. Write out a message."""
err(f"error: cannot format {src}: {message}")
self.failure_count += 1
def path_ignored(self, path: Path, message: str) -> None:
if self.verbose:
out(f"{path} ignored: {message}", bold=False)
@property
def return_code(self) -> int:
"""Return the exit code that the app should use.
This considers the current state of changed files and failures:
- if there were any failures, return 123;
- if any files were changed and --check is being used, return 1;
- otherwise return 0.
"""
# According to http://tldp.org/LDP/abs/html/exitcodes.html starting with
# 126 we have special return codes reserved by the shell.
if self.failure_count:
return 123
elif self.change_count and self.check:
return 1
return 0
def __str__(self) -> str:
"""Render a color report of the current state.
Use `click.unstyle` to remove colors.
"""
if self.check or self.diff:
reformatted = "would be reformatted"
unchanged = "would be left unchanged"
failed = "would fail to reformat"
else:
reformatted = "reformatted"
unchanged = "left unchanged"
failed = "failed to reformat"
report = []
if self.change_count:
s = "s" if self.change_count > 1 else ""
report.append(
style(f"{self.change_count} file{s} ", bold=True, fg="blue")
+ style(f"{reformatted}", bold=True)
)
if self.same_count:
s = "s" if self.same_count > 1 else ""
report.append(style(f"{self.same_count} file{s} ", fg="blue") + unchanged)
if self.failure_count:
s = "s" if self.failure_count > 1 else ""
report.append(style(f"{self.failure_count} file{s} {failed}", fg="red"))
return ", ".join(report) + "."

View file

@ -0,0 +1,154 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://github.com/psf/black/blob/main/src/black/resources/black.schema.json",
"$comment": "tool.black table in pyproject.toml",
"type": "object",
"additionalProperties": false,
"properties": {
"code": {
"type": "string",
"description": "Format the code passed in as a string."
},
"line-length": {
"type": "integer",
"description": "How many characters per line to allow.",
"default": 88
},
"target-version": {
"type": "array",
"items": {
"enum": [
"py33",
"py34",
"py35",
"py36",
"py37",
"py38",
"py39",
"py310",
"py311",
"py312",
"py313",
"py314"
]
},
"description": "Python versions that should be supported by Black's output. You should include all versions that your code supports. By default, Black will infer target versions from the project metadata in pyproject.toml. If this does not yield conclusive results, Black will use per-file auto-detection."
},
"pyi": {
"type": "boolean",
"description": "Format all input files like typing stubs regardless of file extension. This is useful when piping source on standard input.",
"default": false
},
"ipynb": {
"type": "boolean",
"description": "Format all input files like Jupyter Notebooks regardless of file extension. This is useful when piping source on standard input.",
"default": false
},
"python-cell-magics": {
"type": "array",
"items": {
"type": "string"
},
"description": "When processing Jupyter Notebooks, add the given magic to the list of known python-magics (capture, prun, pypy, python, python3, time, timeit). Useful for formatting cells with custom python magics."
},
"skip-source-first-line": {
"type": "boolean",
"description": "Skip the first line of the source code.",
"default": false
},
"skip-string-normalization": {
"type": "boolean",
"description": "Don't normalize string quotes or prefixes.",
"default": false
},
"skip-magic-trailing-comma": {
"type": "boolean",
"description": "Don't use trailing commas as a reason to split lines.",
"default": false
},
"preview": {
"type": "boolean",
"description": "Enable potentially disruptive style changes that may be added to Black's main functionality in the next major release.",
"default": false
},
"unstable": {
"type": "boolean",
"description": "Enable potentially disruptive style changes that have known bugs or are not currently expected to make it into the stable style Black's next major release. Implies --preview.",
"default": false
},
"enable-unstable-feature": {
"type": "array",
"items": {
"enum": [
"string_processing",
"hug_parens_with_braces_and_square_brackets",
"wrap_comprehension_in",
"simplify_power_operator_hugging",
"wrap_long_dict_values_in_parens",
"fix_if_guard_explosion_in_case_statement"
]
},
"description": "Enable specific features included in the `--unstable` style. Requires `--preview`. No compatibility guarantees are provided on the behavior or existence of any unstable features."
},
"check": {
"type": "boolean",
"description": "Don't write the files back, just return the status. Return code 0 means nothing would change. Return code 1 means some files would be reformatted. Return code 123 means there was an internal error.",
"default": false
},
"diff": {
"type": "boolean",
"description": "Don't write the files back, just output a diff to indicate what changes Black would've made. They are printed to stdout so capturing them is simple.",
"default": false
},
"color": {
"type": "boolean",
"description": "Show (or do not show) colored diff. Only applies when --diff is given.",
"default": false
},
"fast": {
"type": "boolean",
"description": "By default, Black performs an AST safety check after formatting your code. The --fast flag turns off this check and the --safe flag explicitly enables it. [default: --safe]",
"default": false
},
"required-version": {
"type": "string",
"description": "Require a specific version of Black to be running. This is useful for ensuring that all contributors to your project are using the same version, because different versions of Black may format code a little differently. This option can be set in a configuration file for consistent results across environments."
},
"exclude": {
"type": "string",
"description": "A regular expression that matches files and directories that should be excluded on recursive searches. An empty value means no paths are excluded. Use forward slashes for directories on all platforms (Windows, too). By default, Black also ignores all paths listed in .gitignore. Changing this value will override all default exclusions. [default: /(\\.direnv|\\.eggs|\\.git|\\.hg|\\.ipynb_checkpoints|\\.mypy_cache|\\.nox|\\.pytest_cache|\\.ruff_cache|\\.tox|\\.svn|\\.venv|\\.vscode|__pypackages__|_build|buck-out|build|dist|venv)/]"
},
"extend-exclude": {
"type": "string",
"description": "Like --exclude, but adds additional files and directories on top of the default values instead of overriding them."
},
"force-exclude": {
"type": "string",
"description": "Like --exclude, but files and directories matching this regex will be excluded even when they are passed explicitly as arguments. This is useful when invoking Black programmatically on changed files, such as in a pre-commit hook or editor plugin."
},
"include": {
"type": "string",
"description": "A regular expression that matches files and directories that should be included on recursive searches. An empty value means all files are included regardless of the name. Use forward slashes for directories on all platforms (Windows, too). Overrides all exclusions, including from .gitignore and command line options.",
"default": "(\\.pyi?|\\.ipynb)$"
},
"workers": {
"type": "integer",
"description": "When Black formats multiple files, it may use a process pool to speed up formatting. This option controls the number of parallel workers. This can also be specified via the BLACK_NUM_WORKERS environment variable. Defaults to the number of CPUs in the system."
},
"quiet": {
"type": "boolean",
"description": "Stop emitting all non-critical output. Error messages will still be emitted (which can silenced by 2>/dev/null).",
"default": false
},
"verbose": {
"type": "boolean",
"description": "Emit messages about files that were not changed or were ignored due to exclusion patterns. If Black is using a configuration file, a message detailing which one it is using will be emitted.",
"default": false
},
"no-cache": {
"type": "boolean",
"description": "Skip reading and writing the cache, forcing Black to reformat all included files.",
"default": false
}
}
}

View file

@ -0,0 +1,28 @@
"""An error-handling model influenced by that used by the Rust programming language
See https://doc.rust-lang.org/book/ch09-00-error-handling.html.
"""
from typing import Generic, TypeVar, Union
T = TypeVar("T")
E = TypeVar("E", bound=Exception)
class Ok(Generic[T]):
def __init__(self, value: T) -> None:
self._value = value
def ok(self) -> T:
return self._value
class Err(Generic[E]):
def __init__(self, e: E) -> None:
self._e = e
def err(self) -> E:
return self._e
Result = Union[Ok[T], Err[E]]

View file

@ -0,0 +1,15 @@
import importlib.resources
import json
from typing import Any
def get_schema(tool_name: str = "black") -> Any:
"""Get the stored complete schema for black's settings."""
assert tool_name == "black", "Only black is supported."
pkg = "black.resources"
fname = "black.schema.json"
schema = importlib.resources.files(pkg).joinpath(fname)
with schema.open(encoding="utf-8") as f:
return json.load(f)

View file

@ -0,0 +1,391 @@
"""
Simple formatting on strings. Further string formatting code is in trans.py.
"""
import re
import sys
from functools import lru_cache
from re import Match, Pattern
from typing import Final
from black._width_table import WIDTH_TABLE
from blib2to3.pytree import Leaf
STRING_PREFIX_CHARS: Final = "fturbFTURB" # All possible string prefix characters.
STRING_PREFIX_RE: Final = re.compile(
r"^([" + STRING_PREFIX_CHARS + r"]*)(.*)$", re.DOTALL
)
UNICODE_ESCAPE_RE: Final = re.compile(
r"(?P<backslashes>\\+)(?P<body>"
r"(u(?P<u>[a-fA-F0-9]{4}))" # Character with 16-bit hex value xxxx
r"|(U(?P<U>[a-fA-F0-9]{8}))" # Character with 32-bit hex value xxxxxxxx
r"|(x(?P<x>[a-fA-F0-9]{2}))" # Character with hex value hh
r"|(N\{(?P<N>[a-zA-Z0-9 \-]{2,})\})" # Character named name in the Unicode database
r")",
re.VERBOSE,
)
def sub_twice(regex: Pattern[str], replacement: str, original: str) -> str:
"""Replace `regex` with `replacement` twice on `original`.
This is used by string normalization to perform replaces on
overlapping matches.
"""
return regex.sub(replacement, regex.sub(replacement, original))
def has_triple_quotes(string: str) -> bool:
"""
Returns:
True iff @string starts with three quotation characters.
"""
raw_string = string.lstrip(STRING_PREFIX_CHARS)
return raw_string[:3] in {'"""', "'''"}
def lines_with_leading_tabs_expanded(s: str) -> list[str]:
"""
Splits string into lines and expands only leading tabs (following the normal
Python rules)
"""
lines = []
for line in s.splitlines():
stripped_line = line.lstrip()
if not stripped_line or stripped_line == line:
lines.append(line)
else:
prefix_length = len(line) - len(stripped_line)
prefix = line[:prefix_length].expandtabs()
lines.append(prefix + stripped_line)
if s.endswith("\n"):
lines.append("")
return lines
def fix_multiline_docstring(docstring: str, prefix: str) -> str:
# https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation
assert docstring, "INTERNAL ERROR: Multiline docstrings cannot be empty"
lines = lines_with_leading_tabs_expanded(docstring)
# Determine minimum indentation (first line doesn't count):
indent = sys.maxsize
for line in lines[1:]:
stripped = line.lstrip()
if stripped:
indent = min(indent, len(line) - len(stripped))
# Remove indentation (first line is special):
trimmed = [lines[0].strip()]
if indent < sys.maxsize:
last_line_idx = len(lines) - 2
for i, line in enumerate(lines[1:]):
stripped_line = line[indent:].rstrip()
if stripped_line or i == last_line_idx:
trimmed.append(prefix + stripped_line)
else:
trimmed.append("")
return "\n".join(trimmed)
def get_string_prefix(string: str) -> str:
"""
Pre-conditions:
* assert_is_leaf_string(@string)
Returns:
@string's prefix (e.g. '', 'r', 'f', or 'rf').
"""
assert_is_leaf_string(string)
prefix = []
for char in string:
if char in STRING_PREFIX_CHARS:
prefix.append(char)
else:
break
return "".join(prefix)
def assert_is_leaf_string(string: str) -> None:
"""
Checks the pre-condition that @string has the format that you would expect
of `leaf.value` where `leaf` is some Leaf such that `leaf.type ==
token.STRING`. A more precise description of the pre-conditions that are
checked are listed below.
Pre-conditions:
* @string starts with either ', ", <prefix>', or <prefix>" where
`set(<prefix>)` is some subset of `set(STRING_PREFIX_CHARS)`.
* @string ends with a quote character (' or ").
Raises:
AssertionError(...) if the pre-conditions listed above are not
satisfied.
"""
dquote_idx = string.find('"')
squote_idx = string.find("'")
if -1 in [dquote_idx, squote_idx]:
quote_idx = max(dquote_idx, squote_idx)
else:
quote_idx = min(squote_idx, dquote_idx)
assert (
0 <= quote_idx < len(string) - 1
), f"{string!r} is missing a starting quote character (' or \")."
assert string[-1] in (
"'",
'"',
), f"{string!r} is missing an ending quote character (' or \")."
assert set(string[:quote_idx]).issubset(
set(STRING_PREFIX_CHARS)
), f"{set(string[:quote_idx])} is NOT a subset of {set(STRING_PREFIX_CHARS)}."
def normalize_string_prefix(s: str) -> str:
"""Make all string prefixes lowercase."""
match = STRING_PREFIX_RE.match(s)
assert match is not None, f"failed to match string {s!r}"
orig_prefix = match.group(1)
new_prefix = (
orig_prefix.replace("F", "f")
.replace("B", "b")
.replace("U", "")
.replace("u", "")
)
# Python syntax guarantees max 2 prefixes and that one of them is "r"
if len(new_prefix) == 2 and new_prefix[0].lower() != "r":
new_prefix = new_prefix[::-1]
return f"{new_prefix}{match.group(2)}"
# Re(gex) does actually cache patterns internally but this still improves
# performance on a long list literal of strings by 5-9% since lru_cache's
# caching overhead is much lower.
@lru_cache(maxsize=64)
def _cached_compile(pattern: str) -> Pattern[str]:
return re.compile(pattern)
def normalize_string_quotes(s: str) -> str:
"""Prefer double quotes but only if it doesn't cause more escaping.
Adds or removes backslashes as appropriate.
"""
value = s.lstrip(STRING_PREFIX_CHARS)
if value[:3] == '"""':
return s
elif value[:3] == "'''":
orig_quote = "'''"
new_quote = '"""'
elif value[0] == '"':
orig_quote = '"'
new_quote = "'"
else:
orig_quote = "'"
new_quote = '"'
first_quote_pos = s.find(orig_quote)
assert first_quote_pos != -1, f"INTERNAL ERROR: Malformed string {s!r}"
prefix = s[:first_quote_pos]
unescaped_new_quote = _cached_compile(rf"(([^\\]|^)(\\\\)*){new_quote}")
escaped_new_quote = _cached_compile(rf"([^\\]|^)\\((?:\\\\)*){new_quote}")
escaped_orig_quote = _cached_compile(rf"([^\\]|^)\\((?:\\\\)*){orig_quote}")
body = s[first_quote_pos + len(orig_quote) : -len(orig_quote)]
if "r" in prefix.casefold():
if unescaped_new_quote.search(body):
# There's at least one unescaped new_quote in this raw string
# so converting is impossible
return s
# Do not introduce or remove backslashes in raw strings
new_body = body
else:
# remove unnecessary escapes
new_body = sub_twice(escaped_new_quote, rf"\1\2{new_quote}", body)
if body != new_body:
# Consider the string without unnecessary escapes as the original
body = new_body
s = f"{prefix}{orig_quote}{body}{orig_quote}"
new_body = sub_twice(escaped_orig_quote, rf"\1\2{orig_quote}", new_body)
new_body = sub_twice(unescaped_new_quote, rf"\1\\{new_quote}", new_body)
if "f" in prefix.casefold():
matches = re.findall(
r"""
(?:(?<!\{)|^)\{ # start of the string or a non-{ followed by a single {
([^{].*?) # contents of the brackets except if begins with {{
\}(?:(?!\})|$) # A } followed by end of the string or a non-}
""",
new_body,
re.VERBOSE,
)
for m in matches:
if "\\" in str(m):
# Do not introduce backslashes in interpolated expressions
return s
if new_quote == '"""' and new_body[-1:] == '"':
# edge case:
new_body = new_body[:-1] + '\\"'
orig_escape_count = body.count("\\")
new_escape_count = new_body.count("\\")
if new_escape_count > orig_escape_count:
return s # Do not introduce more escaping
if new_escape_count == orig_escape_count and orig_quote == '"':
return s # Prefer double quotes
return f"{prefix}{new_quote}{new_body}{new_quote}"
def normalize_fstring_quotes(
quote: str,
middles: list[Leaf],
is_raw_fstring: bool,
) -> tuple[list[Leaf], str]:
"""Prefer double quotes but only if it doesn't cause more escaping.
Adds or removes backslashes as appropriate.
"""
if quote == '"""':
return middles, quote
elif quote == "'''":
new_quote = '"""'
elif quote == '"':
new_quote = "'"
else:
new_quote = '"'
unescaped_new_quote = _cached_compile(rf"(([^\\]|^)(\\\\)*){new_quote}")
escaped_new_quote = _cached_compile(rf"([^\\]|^)\\((?:\\\\)*){new_quote}")
escaped_orig_quote = _cached_compile(rf"([^\\]|^)\\((?:\\\\)*){quote}")
if is_raw_fstring:
for middle in middles:
if unescaped_new_quote.search(middle.value):
# There's at least one unescaped new_quote in this raw string
# so converting is impossible
return middles, quote
# Do not introduce or remove backslashes in raw strings, just use double quote
return middles, '"'
new_segments = []
for middle in middles:
segment = middle.value
# remove unnecessary escapes
new_segment = sub_twice(escaped_new_quote, rf"\1\2{new_quote}", segment)
if segment != new_segment:
# Consider the string without unnecessary escapes as the original
middle.value = new_segment
new_segment = sub_twice(escaped_orig_quote, rf"\1\2{quote}", new_segment)
new_segment = sub_twice(unescaped_new_quote, rf"\1\\{new_quote}", new_segment)
new_segments.append(new_segment)
if new_quote == '"""' and new_segments[-1].endswith('"'):
# edge case:
new_segments[-1] = new_segments[-1][:-1] + '\\"'
orig_escape_count = 0
new_escape_count = 0
for middle, new_segment in zip(middles, new_segments, strict=True):
orig_escape_count += middle.value.count("\\")
new_escape_count += new_segment.count("\\")
if new_escape_count > orig_escape_count:
return middles, quote # Do not introduce more escaping
if new_escape_count == orig_escape_count and quote == '"':
return middles, quote # Prefer double quotes
for middle, new_segment in zip(middles, new_segments, strict=True):
middle.value = new_segment
return middles, new_quote
def normalize_unicode_escape_sequences(leaf: Leaf) -> None:
"""Replace hex codes in Unicode escape sequences with lowercase representation."""
text = leaf.value
prefix = get_string_prefix(text)
if "r" in prefix.lower():
return
def replace(m: Match[str]) -> str:
groups = m.groupdict()
back_slashes = groups["backslashes"]
if len(back_slashes) % 2 == 0:
return back_slashes + groups["body"]
if groups["u"]:
# \u
return back_slashes + "u" + groups["u"].lower()
elif groups["U"]:
# \U
return back_slashes + "U" + groups["U"].lower()
elif groups["x"]:
# \x
return back_slashes + "x" + groups["x"].lower()
else:
assert groups["N"], f"Unexpected match: {m}"
# \N{}
return back_slashes + "N{" + groups["N"].upper() + "}"
leaf.value = re.sub(UNICODE_ESCAPE_RE, replace, text)
@lru_cache(maxsize=4096)
def char_width(char: str) -> int:
"""Return the width of a single character as it would be displayed in a
terminal or editor (which respects Unicode East Asian Width).
Full width characters are counted as 2, while half width characters are
counted as 1. Also control characters are counted as 0.
"""
table = WIDTH_TABLE
codepoint = ord(char)
highest = len(table) - 1
lowest = 0
idx = highest // 2
while True:
start_codepoint, end_codepoint, width = table[idx]
if codepoint < start_codepoint:
highest = idx - 1
elif codepoint > end_codepoint:
lowest = idx + 1
else:
return 0 if width < 0 else width
if highest < lowest:
break
idx = (highest + lowest) // 2
return 1
def str_width(line_str: str) -> int:
"""Return the width of `line_str` as it would be displayed in a terminal
or editor (which respects Unicode East Asian Width).
You could utilize this function to determine, for example, if a string
is too wide to display in a terminal or editor.
"""
if line_str.isascii():
# Fast path for a line consisting of only ASCII characters
return len(line_str)
return sum(map(char_width, line_str))
def count_chars_in_width(line_str: str, max_width: int) -> int:
"""Count the number of characters in `line_str` that would fit in a
terminal or editor of `max_width` (which respects Unicode East Asian
Width).
"""
total_width = 0
for i, char in enumerate(line_str):
width = char_width(char)
if width + total_width > max_width:
return i
total_width += width
return len(line_str)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,334 @@
import asyncio
import logging
import os
from concurrent.futures import Executor, ProcessPoolExecutor
from datetime import datetime, timezone
from functools import cache, partial
from multiprocessing import freeze_support
try:
from aiohttp import web
from multidict import MultiMapping
from .middlewares import cors
except ImportError as ie:
raise ImportError(
f"aiohttp dependency is not installed: {ie}. "
+ "Please re-install black with the '[d]' extra install "
+ "to obtain aiohttp_cors: `pip install black[d]`"
) from None
import click
import black
from _black_version import version as __version__
from black.concurrency import maybe_use_uvloop
# This is used internally by tests to shut down the server prematurely
_stop_signal = asyncio.Event()
# Request headers
PROTOCOL_VERSION_HEADER = "X-Protocol-Version"
LINE_LENGTH_HEADER = "X-Line-Length"
PYTHON_VARIANT_HEADER = "X-Python-Variant"
SKIP_SOURCE_FIRST_LINE = "X-Skip-Source-First-Line"
SKIP_STRING_NORMALIZATION_HEADER = "X-Skip-String-Normalization"
SKIP_MAGIC_TRAILING_COMMA = "X-Skip-Magic-Trailing-Comma"
PREVIEW = "X-Preview"
UNSTABLE = "X-Unstable"
ENABLE_UNSTABLE_FEATURE = "X-Enable-Unstable-Feature"
FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe"
DIFF_HEADER = "X-Diff"
BLACK_HEADERS = [
PROTOCOL_VERSION_HEADER,
LINE_LENGTH_HEADER,
PYTHON_VARIANT_HEADER,
SKIP_SOURCE_FIRST_LINE,
SKIP_STRING_NORMALIZATION_HEADER,
SKIP_MAGIC_TRAILING_COMMA,
PREVIEW,
UNSTABLE,
ENABLE_UNSTABLE_FEATURE,
FAST_OR_SAFE_HEADER,
DIFF_HEADER,
]
# Response headers
BLACK_VERSION_HEADER = "X-Black-Version"
DEFAULT_MAX_BODY_SIZE = 5 * 1024 * 1024
DEFAULT_WORKERS = os.cpu_count() or 1
class HeaderError(Exception):
pass
class InvalidVariantHeader(Exception):
pass
@click.command(context_settings={"help_option_names": ["-h", "--help"]})
@click.option(
"--bind-host",
type=str,
help="Address to bind the server to.",
default="localhost",
show_default=True,
)
@click.option(
"--bind-port", type=int, help="Port to listen on", default=45484, show_default=True
)
@click.option(
"--cors-allow-origin",
"cors_allow_origins",
multiple=True,
help="Origin allowed to access blackd over CORS. Can be passed multiple times.",
)
@click.option(
"--max-body-size",
type=click.IntRange(min=1),
default=DEFAULT_MAX_BODY_SIZE,
show_default=True,
help="Maximum request body size in bytes.",
)
@click.version_option(version=black.__version__)
def main(
bind_host: str,
bind_port: int,
cors_allow_origins: tuple[str, ...],
max_body_size: int,
) -> None:
logging.basicConfig(level=logging.INFO)
app = make_app(cors_allow_origins=cors_allow_origins, max_body_size=max_body_size)
ver = black.__version__
black.out(f"blackd version {ver} listening on {bind_host} port {bind_port}")
loop = maybe_use_uvloop()
try:
web.run_app(
app,
host=bind_host,
port=bind_port,
handle_signals=True,
print=None,
loop=loop,
)
finally:
if not loop.is_closed():
loop.close()
@cache
def executor() -> Executor:
return ProcessPoolExecutor(max_workers=DEFAULT_WORKERS)
def make_app(
*,
cors_allow_origins: tuple[str, ...] = (),
max_body_size: int = DEFAULT_MAX_BODY_SIZE,
) -> web.Application:
app = web.Application(
client_max_size=max_body_size,
middlewares=[
cors(
allow_headers=(*BLACK_HEADERS, "Content-Type"),
allow_origins=frozenset(cors_allow_origins),
expose_headers=(BLACK_VERSION_HEADER,),
)
],
)
app.add_routes([
web.post(
"/",
partial(
handle,
executor=executor(),
executor_semaphore=asyncio.BoundedSemaphore(DEFAULT_WORKERS),
),
)
])
return app
async def handle(
request: web.Request,
executor: Executor,
executor_semaphore: asyncio.BoundedSemaphore,
) -> web.Response:
headers = {BLACK_VERSION_HEADER: __version__}
try:
if request.headers.get(PROTOCOL_VERSION_HEADER, "1") != "1":
return web.Response(
status=501, text="This server only supports protocol version 1"
)
fast = False
if request.headers.get(FAST_OR_SAFE_HEADER, "safe") == "fast":
fast = True
try:
mode = parse_mode(request.headers)
except HeaderError as e:
return web.Response(status=400, text=e.args[0])
req_bytes = await request.read()
charset = request.charset if request.charset is not None else "utf8"
req_str = req_bytes.decode(charset)
then = datetime.now(timezone.utc)
header = ""
if mode.skip_source_first_line:
first_newline_position: int = req_str.find("\n") + 1
header = req_str[:first_newline_position]
req_str = req_str[first_newline_position:]
only_diff = bool(request.headers.get(DIFF_HEADER, False))
formatted_str = await format_code(
req_str=req_str,
fast=fast,
mode=mode,
then=then,
only_diff=only_diff,
executor=executor,
executor_semaphore=executor_semaphore,
)
# Put the source first line back
req_str = header + req_str
formatted_str = header + formatted_str
return web.Response(
content_type=request.content_type,
charset=charset,
headers=headers,
text=formatted_str,
)
except black.NothingChanged:
return web.Response(status=204, headers=headers)
except black.InvalidInput as e:
return web.Response(status=400, headers=headers, text=str(e))
except web.HTTPException:
raise
except Exception as e:
logging.exception("Exception during handling a request")
return web.Response(status=500, headers=headers, text=str(e))
async def format_code(
*,
req_str: str,
fast: bool,
mode: black.FileMode,
then: datetime,
only_diff: bool,
executor: Executor,
executor_semaphore: asyncio.BoundedSemaphore,
) -> str:
async with executor_semaphore:
loop = asyncio.get_event_loop()
formatted_str = await loop.run_in_executor(
executor, partial(black.format_file_contents, req_str, fast=fast, mode=mode)
)
if not only_diff:
return formatted_str
now = datetime.now(timezone.utc)
src_name = f"In\t{then}"
dst_name = f"Out\t{now}"
return await loop.run_in_executor(
executor,
partial(black.diff, req_str, formatted_str, src_name, dst_name),
)
def parse_mode(headers: MultiMapping[str]) -> black.Mode:
try:
line_length = int(headers.get(LINE_LENGTH_HEADER, black.DEFAULT_LINE_LENGTH))
except ValueError:
raise HeaderError("Invalid line length header value") from None
if PYTHON_VARIANT_HEADER in headers:
value = headers[PYTHON_VARIANT_HEADER]
try:
pyi, versions = parse_python_variant_header(value)
except InvalidVariantHeader as e:
raise HeaderError(
f"Invalid value for {PYTHON_VARIANT_HEADER}: {e.args[0]}",
) from None
else:
pyi = False
versions = set()
skip_string_normalization = bool(
headers.get(SKIP_STRING_NORMALIZATION_HEADER, False)
)
skip_magic_trailing_comma = bool(headers.get(SKIP_MAGIC_TRAILING_COMMA, False))
skip_source_first_line = bool(headers.get(SKIP_SOURCE_FIRST_LINE, False))
preview = bool(headers.get(PREVIEW, False))
unstable = bool(headers.get(UNSTABLE, False))
enable_features: set[black.Preview] = set()
enable_unstable_features = headers.get(ENABLE_UNSTABLE_FEATURE, "").split(",")
for piece in enable_unstable_features:
piece = piece.strip()
if piece:
try:
enable_features.add(black.Preview[piece])
except KeyError:
raise HeaderError(
f"Invalid value for {ENABLE_UNSTABLE_FEATURE}: {piece}",
) from None
return black.FileMode(
target_versions=versions,
is_pyi=pyi,
line_length=line_length,
skip_source_first_line=skip_source_first_line,
string_normalization=not skip_string_normalization,
magic_trailing_comma=not skip_magic_trailing_comma,
preview=preview,
unstable=unstable,
enabled_features=enable_features,
)
def parse_python_variant_header(value: str) -> tuple[bool, set[black.TargetVersion]]:
if value == "pyi":
return True, set()
else:
versions = set()
for version in value.split(","):
if version.startswith("py"):
version = version[len("py") :]
if "." in version:
major_str, *rest = version.split(".")
else:
major_str = version[0]
rest = [version[1:]] if len(version) > 1 else []
try:
major = int(major_str)
if major not in (2, 3):
raise InvalidVariantHeader("major version must be 2 or 3")
if len(rest) > 0:
minor = int(rest[0])
if major == 2:
raise InvalidVariantHeader("Python 2 is not supported")
else:
# Default to lowest supported minor version.
minor = 7 if major == 2 else 3
version_str = f"PY{major}{minor}"
if major == 3 and not hasattr(black.TargetVersion, version_str):
raise InvalidVariantHeader(f"3.{minor} is not supported")
versions.add(black.TargetVersion[version_str])
except (KeyError, ValueError):
raise InvalidVariantHeader("expected e.g. '3.7', 'py3.5'") from None
return False, versions
def patched_main() -> None:
freeze_support()
main()
if __name__ == "__main__":
patched_main()

View file

@ -0,0 +1,3 @@
import blackd
blackd.patched_main()

View file

@ -0,0 +1,92 @@
import aiohttp
from aiohttp.typedefs import StrOrURL
import black
_DEFAULT_HEADERS = {"Content-Type": "text/plain; charset=utf-8"}
class BlackDClient:
def __init__(
self,
url: StrOrURL = "http://localhost:9090",
line_length: int | None = None,
skip_source_first_line: bool = False,
skip_string_normalization: bool = False,
skip_magic_trailing_comma: bool = False,
preview: bool = False,
fast: bool = False,
python_variant: str | None = None,
diff: bool = False,
headers: dict[str, str] | None = None,
):
"""
Initialize a BlackDClient object.
:param url: The URL of the BlackD server.
:param line_length: The maximum line length.
Corresponds to the ``--line-length`` CLI option.
:param skip_source_first_line: True to skip the first line of the source.
Corresponds to the ``--skip-source-first-line`` CLI option.
:param skip_string_normalization: True to skip string normalization.
Corresponds to the ``--skip-string-normalization`` CLI option.
:param skip_magic_trailing_comma: True to skip magic trailing comma.
Corresponds to the ``--skip-magic-trailing-comma`` CLI option.
:param preview: True to enable experimental preview mode.
Corresponds to the ``--preview`` CLI option.
:param fast: True to enable fast mode.
Corresponds to the ``--fast`` CLI option.
:param python_variant: The Python variant to use.
Corresponds to the ``--pyi`` CLI option if this is "pyi".
Otherwise, corresponds to the ``--target-version`` CLI option.
:param diff: True to enable diff mode.
Corresponds to the ``--diff`` CLI option.
:param headers: A dictionary of additional custom headers to send with
the request.
"""
self.url = url
self.headers = _DEFAULT_HEADERS.copy()
if line_length is not None:
self.headers["X-Line-Length"] = str(line_length)
if skip_source_first_line:
self.headers["X-Skip-Source-First-Line"] = "yes"
if skip_string_normalization:
self.headers["X-Skip-String-Normalization"] = "yes"
if skip_magic_trailing_comma:
self.headers["X-Skip-Magic-Trailing-Comma"] = "yes"
if preview:
self.headers["X-Preview"] = "yes"
if fast:
self.headers["X-Fast-Or-Safe"] = "fast"
if python_variant is not None:
self.headers["X-Python-Variant"] = python_variant
if diff:
self.headers["X-Diff"] = "yes"
if headers is not None:
self.headers.update(headers)
async def format_code(self, unformatted_code: str) -> str:
async with aiohttp.ClientSession() as session:
async with session.post(
self.url, headers=self.headers, data=unformatted_code.encode("utf-8")
) as response:
if response.status == 204:
# Input is already well-formatted
return unformatted_code
elif response.status == 200:
# Formatting was needed
return await response.text()
elif response.status == 400:
# Input contains a syntax error
error_message = await response.text()
raise black.InvalidInput(error_message)
elif response.status == 500:
# Other kind of error while formatting
error_message = await response.text()
raise RuntimeError(f"Error while formatting: {error_message}")
else:
# Unexpected response status code
raise RuntimeError(
f"Unexpected response status code: {response.status}"
)

View file

@ -0,0 +1,45 @@
from collections.abc import Awaitable, Callable, Collection, Iterable
from aiohttp import web
from aiohttp.typedefs import Middleware
from aiohttp.web_middlewares import middleware
from aiohttp.web_request import Request
from aiohttp.web_response import StreamResponse
Handler = Callable[[Request], Awaitable[StreamResponse]]
def cors(
*,
allow_headers: Iterable[str],
allow_origins: Collection[str],
expose_headers: Iterable[str],
) -> Middleware:
@middleware
async def impl(request: Request, handler: Handler) -> StreamResponse:
origin = request.headers.get("Origin")
if not origin:
return await handler(request)
if origin not in allow_origins:
return web.Response(status=403, text="CORS origin is not allowed")
is_options = request.method == "OPTIONS"
is_preflight = is_options and "Access-Control-Request-Method" in request.headers
if is_preflight:
resp = StreamResponse()
else:
resp = await handler(request)
resp.headers["Access-Control-Allow-Origin"] = origin
if expose_headers:
resp.headers["Access-Control-Expose-Headers"] = ", ".join(expose_headers)
if is_options:
resp.headers["Access-Control-Allow-Headers"] = ", ".join(allow_headers)
resp.headers["Access-Control-Allow-Methods"] = ", ".join(
("OPTIONS", "POST")
)
return resp
return impl

View file

@ -0,0 +1,266 @@
# Grammar for 2to3. This grammar supports Python 2.x and 3.x.
# NOTE WELL: You should also follow all the steps listed at
# https://devguide.python.org/grammar/
# Start symbols for the grammar:
# file_input is a module or sequence of commands read from an input file;
# single_input is a single interactive statement;
# eval_input is the input for the eval() and input() functions.
# NB: compound_stmt in single_input is followed by extra NEWLINE!
file_input: (NEWLINE | stmt)* ENDMARKER
single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE
eval_input: testlist NEWLINE* ENDMARKER
typevar: NAME [':' test] ['=' test]
paramspec: '**' NAME ['=' test]
typevartuple: '*' NAME ['=' (test|star_expr)]
typeparam: typevar | paramspec | typevartuple
typeparams: '[' typeparam (',' typeparam)* [','] ']'
decorator: '@' namedexpr_test NEWLINE
decorators: decorator+
decorated: decorators (classdef | funcdef | async_funcdef)
async_funcdef: ASYNC funcdef
funcdef: 'def' NAME [typeparams] parameters ['->' test] ':' suite
parameters: '(' [typedargslist] ')'
# The following definition for typedarglist is equivalent to this set of rules:
#
# arguments = argument (',' argument)*
# argument = tfpdef ['=' test]
# kwargs = '**' tname [',']
# args = '*' [tname_star]
# kwonly_kwargs = (',' argument)* [',' [kwargs]]
# args_kwonly_kwargs = args kwonly_kwargs | kwargs
# poskeyword_args_kwonly_kwargs = arguments [',' [args_kwonly_kwargs]]
# typedargslist_no_posonly = poskeyword_args_kwonly_kwargs | args_kwonly_kwargs
# typedarglist = arguments ',' '/' [',' [typedargslist_no_posonly]])|(typedargslist_no_posonly)"
#
# It needs to be fully expanded to allow our LL(1) parser to work on it.
typedargslist: tfpdef ['=' test] (',' tfpdef ['=' test])* ',' '/' [
',' [((tfpdef ['=' test] ',')* ('*' [tname_star] (',' tname ['=' test])*
[',' ['**' tname [',']]] | '**' tname [','])
| tfpdef ['=' test] (',' tfpdef ['=' test])* [','])]
] | ((tfpdef ['=' test] ',')* ('*' [tname_star] (',' tname ['=' test])*
[',' ['**' tname [',']]] | '**' tname [','])
| tfpdef ['=' test] (',' tfpdef ['=' test])* [','])
tname: NAME [':' test]
tname_star: NAME [':' (test|star_expr)]
tfpdef: tname | '(' tfplist ')'
tfplist: tfpdef (',' tfpdef)* [',']
# The following definition for varargslist is equivalent to this set of rules:
#
# arguments = argument (',' argument )*
# argument = vfpdef ['=' test]
# kwargs = '**' vname [',']
# args = '*' [vname]
# kwonly_kwargs = (',' argument )* [',' [kwargs]]
# args_kwonly_kwargs = args kwonly_kwargs | kwargs
# poskeyword_args_kwonly_kwargs = arguments [',' [args_kwonly_kwargs]]
# vararglist_no_posonly = poskeyword_args_kwonly_kwargs | args_kwonly_kwargs
# varargslist = arguments ',' '/' [','[(vararglist_no_posonly)]] | (vararglist_no_posonly)
#
# It needs to be fully expanded to allow our LL(1) parser to work on it.
varargslist: vfpdef ['=' test ](',' vfpdef ['=' test])* ',' '/' [',' [
((vfpdef ['=' test] ',')* ('*' [vname] (',' vname ['=' test])*
[',' ['**' vname [',']]] | '**' vname [','])
| vfpdef ['=' test] (',' vfpdef ['=' test])* [','])
]] | ((vfpdef ['=' test] ',')*
('*' [vname] (',' vname ['=' test])* [',' ['**' vname [',']]]| '**' vname [','])
| vfpdef ['=' test] (',' vfpdef ['=' test])* [','])
vname: NAME
vfpdef: vname | '(' vfplist ')'
vfplist: vfpdef (',' vfpdef)* [',']
stmt: simple_stmt | compound_stmt
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
small_stmt: (type_stmt | expr_stmt | del_stmt | pass_stmt | flow_stmt |
import_stmt | global_stmt | assert_stmt)
expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
('=' (yield_expr|testlist_star_expr))*)
annassign: ':' test ['=' (yield_expr|testlist_star_expr)]
testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [',']
augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^=' |
'<<=' | '>>=' | '**=' | '//=')
# For normal and annotated assignments, additional restrictions enforced by the interpreter
del_stmt: 'del' exprlist
pass_stmt: 'pass'
flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt
break_stmt: 'break'
continue_stmt: 'continue'
return_stmt: 'return' [testlist_star_expr]
yield_stmt: yield_expr
raise_stmt: 'raise' [test ['from' test | ',' test [',' test]]]
import_stmt: import_name | import_from
import_name: 'import' dotted_as_names
import_from: ('from' ('.'* dotted_name | '.'+)
'import' ('*' | '(' import_as_names ')' | import_as_names))
import_as_name: NAME ['as' NAME]
dotted_as_name: dotted_name ['as' NAME]
import_as_names: import_as_name (',' import_as_name)* [',']
dotted_as_names: dotted_as_name (',' dotted_as_name)*
dotted_name: NAME ('.' NAME)*
global_stmt: ('global' | 'nonlocal') NAME (',' NAME)*
assert_stmt: 'assert' test [',' test]
type_stmt: "type" NAME [typeparams] '=' test
compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt | match_stmt
async_stmt: ASYNC (funcdef | with_stmt | for_stmt)
if_stmt: 'if' namedexpr_test ':' suite ('elif' namedexpr_test ':' suite)* ['else' ':' suite]
while_stmt: 'while' namedexpr_test ':' suite ['else' ':' suite]
for_stmt: 'for' exprlist 'in' testlist_star_expr ':' suite ['else' ':' suite]
try_stmt: ('try' ':' suite
((except_clause ':' suite)+
['else' ':' suite]
['finally' ':' suite] |
'finally' ':' suite))
with_stmt: 'with' asexpr_test (',' asexpr_test)* ':' suite
# NB compile.c makes sure that the default except clause is last
except_clause: 'except' ['*'] [testlist ['as' test]]
suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT
# Backward compatibility cruft to support:
# [ x for x in lambda: True, lambda: False if x() ]
# even while also allowing:
# lambda x: 5 if x else 2
# (But not a mix of the two)
testlist_safe: old_test [(',' old_test)+ [',']]
old_test: or_test | old_lambdef
old_lambdef: 'lambda' [varargslist] ':' old_test
namedexpr_test: asexpr_test [':=' asexpr_test]
# This is actually not a real rule, though since the parser is very
# limited in terms of the strategy about match/case rules, we are inserting
# a virtual case (<expr> as <expr>) as a valid expression. Unless a better
# approach is thought, the only side effect of this seem to be just allowing
# more stuff to be parser (which would fail on the ast).
asexpr_test: test ['as' test]
test: or_test ['if' or_test 'else' test] | lambdef
or_test: and_test ('or' and_test)*
and_test: not_test ('and' not_test)*
not_test: 'not' not_test | comparison
comparison: expr (comp_op expr)*
comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not'
star_expr: '*' expr
expr: xor_expr ('|' xor_expr)*
xor_expr: and_expr ('^' and_expr)*
and_expr: shift_expr ('&' shift_expr)*
shift_expr: arith_expr (('<<'|'>>') arith_expr)*
arith_expr: term (('+'|'-') term)*
term: factor (('*'|'@'|'/'|'%'|'//') factor)*
factor: ('+'|'-'|'~') factor | power
power: [AWAIT] atom trailer* ['**' factor]
atom: ('(' [yield_expr|testlist_gexp] ')' |
'[' [listmaker] ']' |
'{' [dictsetmaker] '}' |
'`' testlist1 '`' |
NAME | NUMBER | (STRING | fstring | tstring)+ | '.' '.' '.')
listmaker: (namedexpr_test|star_expr) ( old_comp_for | (',' (namedexpr_test|star_expr))* [','] )
testlist_gexp: (namedexpr_test|star_expr) ( old_comp_for | (',' (namedexpr_test|star_expr))* [','] )
lambdef: 'lambda' [varargslist] ':' test
trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME
subscriptlist: (subscript|star_expr) (',' (subscript|star_expr))* [',']
subscript: test [':=' test] | [test] ':' [test] [sliceop]
sliceop: ':' [test]
exprlist: (expr|star_expr) (',' (expr|star_expr))* [',']
testlist: test (',' test)* [',']
dictsetmaker: ( ((test ':' asexpr_test | '**' expr)
(comp_for | (',' (test ':' asexpr_test | '**' expr))* [','])) |
((test [':=' test] | star_expr)
(comp_for | (',' (test [':=' test] | star_expr))* [','])) )
classdef: 'class' NAME [typeparams] ['(' [arglist] ')'] ':' suite
arglist: argument (',' argument)* [',']
# "test '=' test" is really "keyword '=' test", but we have no such token.
# These need to be in a single rule to avoid grammar that is ambiguous
# to our LL(1) parser. Even though 'test' includes '*expr' in star_expr,
# we explicitly match '*' here, too, to give it proper precedence.
# Illegal combinations and orderings are blocked in ast.c:
# multiple (test comp_for) arguments are blocked; keyword unpackings
# that precede iterable unpackings are blocked; etc.
argument: ( test [comp_for] |
test ':=' test [comp_for] |
test 'as' test |
test '=' asexpr_test |
'**' test |
'*' test )
comp_iter: comp_for | comp_if
comp_for: [ASYNC] 'for' exprlist 'in' or_test [comp_iter]
comp_if: 'if' old_test [comp_iter]
# As noted above, testlist_safe extends the syntax allowed in list
# comprehensions and generators. We can't use it indiscriminately in all
# derivations using a comp_for-like pattern because the testlist_safe derivation
# contains comma which clashes with trailing comma in arglist.
#
# This was an issue because the parser would not follow the correct derivation
# when parsing syntactically valid Python code. Since testlist_safe was created
# specifically to handle list comprehensions and generator expressions enclosed
# with parentheses, it's safe to only use it in those. That avoids the issue; we
# can parse code like set(x for x in [],).
#
# The syntax supported by this set of rules is not a valid Python 3 syntax,
# hence the prefix "old".
#
# See https://bugs.python.org/issue27494
old_comp_iter: old_comp_for | old_comp_if
old_comp_for: [ASYNC] 'for' exprlist 'in' testlist_safe [old_comp_iter]
old_comp_if: 'if' old_test [old_comp_iter]
testlist1: test (',' test)*
# not used in grammar, but may appear in "node" passed from Parser to Compiler
encoding_decl: NAME
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist_star_expr
# 3.10 match statement definition
# PS: normally the grammar is much much more restricted, but
# at this moment for not trying to bother much with encoding the
# exact same DSL in a LL(1) parser, we will just accept an expression
# and let the ast.parse() step of the safe mode to reject invalid
# grammar.
# The reason why it is more restricted is that, patterns are some
# sort of a DSL (more advanced than our LHS on assignments, but
# still in a very limited python subset). They are not really
# expressions, but who cares. If we can parse them, that is enough
# to reformat them.
match_stmt: "match" subject_expr ':' NEWLINE INDENT case_block+ DEDENT
# This is more permissive than the actual version. For example it
# accepts `match *something:`, even though single-item starred expressions
# are forbidden.
subject_expr: (namedexpr_test|star_expr) (',' (namedexpr_test|star_expr))* [',']
# cases
case_block: "case" patterns [guard] ':' suite
guard: 'if' namedexpr_test
patterns: pattern (',' pattern)* [',']
pattern: (expr|star_expr) ['as' expr]
fstring: FSTRING_START fstring_middle* FSTRING_END
fstring_middle: fstring_replacement_field | FSTRING_MIDDLE
fstring_replacement_field: '{' (yield_expr | testlist_star_expr) ['='] [ "!" NAME ] [ ':' fstring_format_spec* ] '}'
fstring_format_spec: FSTRING_MIDDLE | fstring_replacement_field
tstring: TSTRING_START tstring_middle* TSTRING_END
tstring_middle: tstring_replacement_field | TSTRING_MIDDLE
tstring_replacement_field: '{' (yield_expr | testlist_star_expr) ['='] [ "!" NAME ] [ ':' tstring_format_spec* ] '}'
tstring_format_spec: TSTRING_MIDDLE | tstring_replacement_field

View file

@ -0,0 +1,254 @@
A. HISTORY OF THE SOFTWARE
==========================
Python was created in the early 1990s by Guido van Rossum at Stichting
Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
as a successor of a language called ABC. Guido remains Python's
principal author, although it includes many contributions from others.
In 1995, Guido continued his work on Python at the Corporation for
National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
in Reston, Virginia where he released several versions of the
software.
In May 2000, Guido and the Python core development team moved to
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
year, the PythonLabs team moved to Digital Creations, which became
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
https://www.python.org/psf/) was formed, a non-profit organization
created specifically to own Python-related Intellectual Property.
Zope Corporation was a sponsoring member of the PSF.
All Python releases are Open Source (see https://opensource.org for
the Open Source Definition). Historically, most, but not all, Python
releases have also been GPL-compatible; the table below summarizes
the various releases.
Release Derived Year Owner GPL-
from compatible? (1)
0.9.0 thru 1.2 1991-1995 CWI yes
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
1.6 1.5.2 2000 CNRI no
2.0 1.6 2000 BeOpen.com no
1.6.1 1.6 2001 CNRI yes (2)
2.1 2.0+1.6.1 2001 PSF no
2.0.1 2.0+1.6.1 2001 PSF yes
2.1.1 2.1+2.0.1 2001 PSF yes
2.1.2 2.1.1 2002 PSF yes
2.1.3 2.1.2 2002 PSF yes
2.2 and above 2.1.1 2001-now PSF yes
Footnotes:
(1) GPL-compatible doesn't mean that we're distributing Python under
the GPL. All Python licenses, unlike the GPL, let you distribute
a modified version without making your changes open source. The
GPL-compatible licenses make it possible to combine Python with
other software that is released under the GPL; the others don't.
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
because its license has a choice of law clause. According to
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
is "not incompatible" with the GPL.
Thanks to the many outside volunteers who have worked under Guido's
direction to make these releases possible.
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
===============================================================
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF hereby
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
analyze, test, perform and/or display publicly, prepare derivative works,
distribute, and otherwise use Python alone or in any derivative version,
provided, however, that PSF's License Agreement and PSF's notice of copyright,
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Python Software Foundation; All
Rights Reserved" are retained in Python alone or in any derivative version
prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
-------------------------------------------
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
Individual or Organization ("Licensee") accessing and otherwise using
this software in source or binary form and its associated
documentation ("the Software").
2. Subject to the terms and conditions of this BeOpen Python License
Agreement, BeOpen hereby grants Licensee a non-exclusive,
royalty-free, world-wide license to reproduce, analyze, test, perform
and/or display publicly, prepare derivative works, distribute, and
otherwise use the Software alone or in any derivative version,
provided, however, that the BeOpen Python License is retained in the
Software, alone or in any derivative version prepared by Licensee.
3. BeOpen is making the Software available to Licensee on an "AS IS"
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
5. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
6. This License Agreement shall be governed by and interpreted in all
respects by the law of the State of California, excluding conflict of
law provisions. Nothing in this License Agreement shall be deemed to
create any relationship of agency, partnership, or joint venture
between BeOpen and Licensee. This License Agreement does not grant
permission to use BeOpen trademarks or trade names in a trademark
sense to endorse or promote products or services of Licensee, or any
third party. As an exception, the "BeOpen Python" logos available at
http://www.pythonlabs.com/logos.html may be used according to the
permissions granted on that web page.
7. By copying, installing or otherwise using the software, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
---------------------------------------
1. This LICENSE AGREEMENT is between the Corporation for National
Research Initiatives, having an office at 1895 Preston White Drive,
Reston, VA 20191 ("CNRI"), and the Individual or Organization
("Licensee") accessing and otherwise using Python 1.6.1 software in
source or binary form and its associated documentation.
2. Subject to the terms and conditions of this License Agreement, CNRI
hereby grants Licensee a nonexclusive, royalty-free, world-wide
license to reproduce, analyze, test, perform and/or display publicly,
prepare derivative works, distribute, and otherwise use Python 1.6.1
alone or in any derivative version, provided, however, that CNRI's
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
1995-2001 Corporation for National Research Initiatives; All Rights
Reserved" are retained in Python 1.6.1 alone or in any derivative
version prepared by Licensee. Alternately, in lieu of CNRI's License
Agreement, Licensee may substitute the following text (omitting the
quotes): "Python 1.6.1 is made available subject to the terms and
conditions in CNRI's License Agreement. This Agreement together with
Python 1.6.1 may be located on the Internet using the following
unique, persistent identifier (known as a handle): 1895.22/1013. This
Agreement may also be obtained from a proxy server on the Internet
using the following URL: http://hdl.handle.net/1895.22/1013".
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python 1.6.1 or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python 1.6.1.
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. This License Agreement shall be governed by the federal
intellectual property law of the United States, including without
limitation the federal copyright law, and, to the extent such
U.S. federal law does not apply, by the law of the Commonwealth of
Virginia, excluding Virginia's conflict of law provisions.
Notwithstanding the foregoing, with regard to derivative works based
on Python 1.6.1 that incorporate non-separable material that was
previously distributed under the GNU General Public License (GPL), the
law of the Commonwealth of Virginia shall govern this License
Agreement only as to issues arising under or with respect to
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
License Agreement shall be deemed to create any relationship of
agency, partnership, or joint venture between CNRI and Licensee. This
License Agreement does not grant permission to use CNRI trademarks or
trade name in a trademark sense to endorse or promote products or
services of Licensee, or any third party.
8. By clicking on the "ACCEPT" button where indicated, or by copying,
installing or otherwise using Python 1.6.1, Licensee agrees to be
bound by the terms and conditions of this License Agreement.
ACCEPT
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
--------------------------------------------------
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
The Netherlands. All rights reserved.
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and that
both that copyright notice and this permission notice appear in
supporting documentation, and that the name of Stichting Mathematisch
Centrum or CWI not be used in advertising or publicity pertaining to
distribution of the software without specific, written prior
permission.
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View file

@ -0,0 +1,28 @@
# Copyright 2006 Google, Inc. All Rights Reserved.
# Licensed to PSF under a Contributor Agreement.
# A grammar to describe tree matching patterns.
# Not shown here:
# - 'TOKEN' stands for any token (leaf node)
# - 'any' stands for any node (leaf or interior)
# With 'any' we can still specify the sub-structure.
# The start symbol is 'Matcher'.
Matcher: Alternatives ENDMARKER
Alternatives: Alternative ('|' Alternative)*
Alternative: (Unit | NegatedUnit)+
Unit: [NAME '='] ( STRING [Repeater]
| NAME [Details] [Repeater]
| '(' Alternatives ')' [Repeater]
| '[' Alternatives ']'
)
NegatedUnit: 'not' (STRING | NAME [Details] | '(' Alternatives ')')
Repeater: '*' | '+' | '{' NUMBER [',' NUMBER] '}'
Details: '<' Alternatives '>'

View file

@ -0,0 +1,24 @@
A subset of lib2to3 taken from Python 3.7.0b2. Commit hash:
9c17e3a1987004b8bcfbe423953aad84493a7984
Reasons for forking:
- consistent handling of f-strings for users of Python < 3.6.2
- backport of BPO-33064 that fixes parsing files with trailing commas after \*args and
\*\*kwargs
- backport of GH-6143 that restores the ability to reformat legacy usage of `async`
- support all types of string literals
- better ability to debug (better reprs)
- INDENT and DEDENT don't hold whitespace and comment prefixes
- ability to Cythonize
Change Log:
- Changes default logger used by Driver
- Backported the following upstream parser changes:
- "bpo-42381: Allow walrus in set literals and set comprehensions (GH-23332)"
https://github.com/python/cpython/commit/cae60187cf7a7b26281d012e1952fafe4e2e97e9
- "bpo-42316: Allow unparenthesized walrus operator in indexes (GH-23317)"
https://github.com/python/cpython/commit/b0aba1fcdc3da952698d99aec2334faa79a8b68c
- Tweaks to help mypyc compile faster code (including inlining type information,
"Final-ing", etc.)

View file

@ -0,0 +1 @@
# empty

View file

@ -0,0 +1,4 @@
# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
# Licensed to PSF under a Contributor Agreement.
"""The pgen2 package."""

View file

@ -0,0 +1,256 @@
# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
# Licensed to PSF under a Contributor Agreement.
# mypy: ignore-errors
"""Convert graminit.[ch] spit out by pgen to Python code.
Pgen is the Python parser generator. It is useful to quickly create a
parser from a grammar file in Python's grammar notation. But I don't
want my parsers to be written in C (yet), so I'm translating the
parsing tables to Python data structures and writing a Python parse
engine.
Note that the token numbers are constants determined by the standard
Python tokenizer. The standard token module defines these numbers and
their names (the names are not used much). The token numbers are
hardcoded into the Python tokenizer and into pgen. A Python
implementation of the Python tokenizer is also available, in the
standard tokenize module.
On the other hand, symbol numbers (representing the grammar's
non-terminals) are assigned by pgen based on the actual grammar
input.
Note: this module is pretty much obsolete; the pgen module generates
equivalent grammar tables directly from the Grammar.txt input file
without having to invoke the Python pgen C program.
"""
# Python imports
import re
# Local imports
from blib2to3.pgen2 import grammar, token
class Converter(grammar.Grammar):
"""Grammar subclass that reads classic pgen output files.
The run() method reads the tables as produced by the pgen parser
generator, typically contained in two C files, graminit.h and
graminit.c. The other methods are for internal use only.
See the base class for more documentation.
"""
def run(self, graminit_h, graminit_c):
"""Load the grammar tables from the text files written by pgen."""
self.parse_graminit_h(graminit_h)
self.parse_graminit_c(graminit_c)
self.finish_off()
def parse_graminit_h(self, filename):
"""Parse the .h file written by pgen. (Internal)
This file is a sequence of #define statements defining the
nonterminals of the grammar as numbers. We build two tables
mapping the numbers to names and back.
"""
try:
f = open(filename)
except OSError as err:
print(f"Can't open {filename}: {err}")
return False
self.symbol2number = {}
self.number2symbol = {}
lineno = 0
for line in f:
lineno += 1
mo = re.match(r"^#define\s+(\w+)\s+(\d+)$", line)
if not mo and line.strip():
print(f"{filename}({lineno}): can't parse {line.strip()}")
else:
symbol, number = mo.groups()
number = int(number)
assert symbol not in self.symbol2number
assert number not in self.number2symbol
self.symbol2number[symbol] = number
self.number2symbol[number] = symbol
return True
def parse_graminit_c(self, filename):
"""Parse the .c file written by pgen. (Internal)
The file looks as follows. The first two lines are always this:
#include "pgenheaders.h"
#include "grammar.h"
After that come four blocks:
1) one or more state definitions
2) a table defining dfas
3) a table defining labels
4) a struct defining the grammar
A state definition has the following form:
- one or more arc arrays, each of the form:
static arc arcs_<n>_<m>[<k>] = {
{<i>, <j>},
...
};
- followed by a state array, of the form:
static state states_<s>[<t>] = {
{<k>, arcs_<n>_<m>},
...
};
"""
try:
f = open(filename)
except OSError as err:
print(f"Can't open {filename}: {err}")
return False
# The code below essentially uses f's iterator-ness!
lineno = 0
# Expect the two #include lines
lineno, line = lineno + 1, next(f)
assert line == '#include "pgenheaders.h"\n', (lineno, line)
lineno, line = lineno + 1, next(f)
assert line == '#include "grammar.h"\n', (lineno, line)
# Parse the state definitions
lineno, line = lineno + 1, next(f)
allarcs = {}
states = []
while line.startswith("static arc "):
while line.startswith("static arc "):
mo = re.match(r"static arc arcs_(\d+)_(\d+)\[(\d+)\] = {$", line)
assert mo, (lineno, line)
n, m, k = list(map(int, mo.groups()))
arcs = []
for _ in range(k):
lineno, line = lineno + 1, next(f)
mo = re.match(r"\s+{(\d+), (\d+)},$", line)
assert mo, (lineno, line)
i, j = list(map(int, mo.groups()))
arcs.append((i, j))
lineno, line = lineno + 1, next(f)
assert line == "};\n", (lineno, line)
allarcs[(n, m)] = arcs
lineno, line = lineno + 1, next(f)
mo = re.match(r"static state states_(\d+)\[(\d+)\] = {$", line)
assert mo, (lineno, line)
s, t = list(map(int, mo.groups()))
assert s == len(states), (lineno, line)
state = []
for _ in range(t):
lineno, line = lineno + 1, next(f)
mo = re.match(r"\s+{(\d+), arcs_(\d+)_(\d+)},$", line)
assert mo, (lineno, line)
k, n, m = list(map(int, mo.groups()))
arcs = allarcs[n, m]
assert k == len(arcs), (lineno, line)
state.append(arcs)
states.append(state)
lineno, line = lineno + 1, next(f)
assert line == "};\n", (lineno, line)
lineno, line = lineno + 1, next(f)
self.states = states
# Parse the dfas
dfas = {}
mo = re.match(r"static dfa dfas\[(\d+)\] = {$", line)
assert mo, (lineno, line)
ndfas = int(mo.group(1))
for i in range(ndfas):
lineno, line = lineno + 1, next(f)
mo = re.match(r'\s+{(\d+), "(\w+)", (\d+), (\d+), states_(\d+),$', line)
assert mo, (lineno, line)
symbol = mo.group(2)
number, x, y, z = list(map(int, mo.group(1, 3, 4, 5)))
assert self.symbol2number[symbol] == number, (lineno, line)
assert self.number2symbol[number] == symbol, (lineno, line)
assert x == 0, (lineno, line)
state = states[z]
assert y == len(state), (lineno, line)
lineno, line = lineno + 1, next(f)
mo = re.match(r'\s+("(?:\\\d\d\d)*")},$', line)
assert mo, (lineno, line)
first = {}
rawbitset = eval(mo.group(1))
for i, c in enumerate(rawbitset):
byte = ord(c)
for j in range(8):
if byte & (1 << j):
first[i * 8 + j] = 1
dfas[number] = (state, first)
lineno, line = lineno + 1, next(f)
assert line == "};\n", (lineno, line)
self.dfas = dfas
# Parse the labels
labels = []
lineno, line = lineno + 1, next(f)
mo = re.match(r"static label labels\[(\d+)\] = {$", line)
assert mo, (lineno, line)
nlabels = int(mo.group(1))
for i in range(nlabels):
lineno, line = lineno + 1, next(f)
mo = re.match(r'\s+{(\d+), (0|"\w+")},$', line)
assert mo, (lineno, line)
x, y = mo.groups()
x = int(x)
if y == "0":
y = None
else:
y = eval(y)
labels.append((x, y))
lineno, line = lineno + 1, next(f)
assert line == "};\n", (lineno, line)
self.labels = labels
# Parse the grammar struct
lineno, line = lineno + 1, next(f)
assert line == "grammar _PyParser_Grammar = {\n", (lineno, line)
lineno, line = lineno + 1, next(f)
mo = re.match(r"\s+(\d+),$", line)
assert mo, (lineno, line)
ndfas = int(mo.group(1))
assert ndfas == len(self.dfas)
lineno, line = lineno + 1, next(f)
assert line == "\tdfas,\n", (lineno, line)
lineno, line = lineno + 1, next(f)
mo = re.match(r"\s+{(\d+), labels},$", line)
assert mo, (lineno, line)
nlabels = int(mo.group(1))
assert nlabels == len(self.labels), (lineno, line)
lineno, line = lineno + 1, next(f)
mo = re.match(r"\s+(\d+)$", line)
assert mo, (lineno, line)
start = int(mo.group(1))
assert start in self.number2symbol, (lineno, line)
self.start = start
lineno, line = lineno + 1, next(f)
assert line == "};\n", (lineno, line)
try:
lineno, line = lineno + 1, next(f)
except StopIteration:
pass
else:
assert 0, (lineno, line)
def finish_off(self):
"""Create additional useful structures. (Internal)."""
self.keywords = {} # map from keyword strings to arc labels
self.tokens = {} # map from numeric token values to arc labels
for ilabel, (type, value) in enumerate(self.labels):
if type == token.NAME and value is not None:
self.keywords[value] = ilabel
elif value is None:
self.tokens[type] = ilabel

View file

@ -0,0 +1,313 @@
# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
# Licensed to PSF under a Contributor Agreement.
# Modifications:
# Copyright 2006 Google, Inc. All Rights Reserved.
# Licensed to PSF under a Contributor Agreement.
"""Parser driver.
This provides a high-level interface to parse a file into a syntax tree.
"""
__author__ = "Guido van Rossum <guido@python.org>"
__all__ = ["Driver", "load_grammar"]
# Python imports
import io
import logging
import os
import pkgutil
import sys
from collections.abc import Iterable, Iterator
from contextlib import contextmanager
from dataclasses import dataclass, field
from logging import Logger
from typing import Any, Union, cast
from blib2to3.pgen2.grammar import Grammar
from blib2to3.pgen2.tokenize import TokenInfo
from blib2to3.pytree import NL
# Pgen imports
from . import grammar, parse, pgen, token, tokenize
Path = Union[str, "os.PathLike[str]"]
@dataclass
class ReleaseRange:
start: int
end: int | None = None
tokens: list[Any] = field(default_factory=list)
def lock(self) -> None:
total_eaten = len(self.tokens)
self.end = self.start + total_eaten
class TokenProxy:
def __init__(self, generator: Any) -> None:
self._tokens = generator
self._counter = 0
self._release_ranges: list[ReleaseRange] = []
@contextmanager
def release(self) -> Iterator["TokenProxy"]:
release_range = ReleaseRange(self._counter)
self._release_ranges.append(release_range)
try:
yield self
finally:
# Lock the last release range to the final position that
# has been eaten.
release_range.lock()
def eat(self, point: int) -> Any:
eaten_tokens = self._release_ranges[-1].tokens
if point < len(eaten_tokens):
return eaten_tokens[point]
else:
while point >= len(eaten_tokens):
token = next(self._tokens)
eaten_tokens.append(token)
return token
def __iter__(self) -> "TokenProxy":
return self
def __next__(self) -> Any:
# If the current position is already compromised (looked up)
# return the eaten token, if not just go further on the given
# token producer.
for release_range in self._release_ranges:
assert release_range.end is not None
start, end = release_range.start, release_range.end
if start <= self._counter < end:
token = release_range.tokens[self._counter - start]
break
else:
token = next(self._tokens)
self._counter += 1
return token
def can_advance(self, to: int) -> bool:
# Try to eat, fail if it can't. The eat operation is cached
# so there won't be any additional cost of eating here
try:
self.eat(to)
except StopIteration:
return False
else:
return True
class Driver:
def __init__(self, grammar: Grammar, logger: Logger | None = None) -> None:
self.grammar = grammar
if logger is None:
logger = logging.getLogger(__name__)
self.logger = logger
def parse_tokens(self, tokens: Iterable[TokenInfo], debug: bool = False) -> NL:
"""Parse a series of tokens and return the syntax tree."""
# XXX Move the prefix computation into a wrapper around tokenize.
proxy = TokenProxy(tokens)
p = parse.Parser(self.grammar)
p.setup(proxy=proxy)
lineno = 1
column = 0
indent_columns: list[int] = []
type = value = start = end = line_text = None
prefix = ""
for quintuple in proxy:
type, value, start, end, line_text = quintuple
if start != (lineno, column):
assert (lineno, column) <= start, ((lineno, column), start)
s_lineno, s_column = start
if lineno < s_lineno:
prefix += "\n" * (s_lineno - lineno)
lineno = s_lineno
column = 0
if column < s_column:
prefix += line_text[column:s_column]
column = s_column
if type in (tokenize.COMMENT, tokenize.NL):
prefix += value
lineno, column = end
if value.endswith("\n"):
lineno += 1
column = 0
continue
if type == token.OP:
type = grammar.opmap[value]
if debug:
assert type is not None
self.logger.debug(
"%s %r (prefix=%r)", token.tok_name[type], value, prefix
)
if type == token.INDENT:
indent_columns.append(len(value))
_prefix = prefix + value
prefix = ""
value = ""
elif type == token.DEDENT:
_indent_col = indent_columns.pop()
prefix, _prefix = self._partially_consume_prefix(prefix, _indent_col)
if p.addtoken(cast(int, type), value, (prefix, start)):
if debug:
self.logger.debug("Stop.")
break
prefix = ""
if type in {token.INDENT, token.DEDENT}:
prefix = _prefix
lineno, column = end
# FSTRING_MIDDLE and TSTRING_MIDDLE are the only token that can end with a
# newline, and `end` will point to the next line. For that case, don't
# increment lineno.
if value.endswith("\n") and type not in (
token.FSTRING_MIDDLE,
token.TSTRING_MIDDLE,
):
lineno += 1
column = 0
else:
# We never broke out -- EOF is too soon (how can this happen???)
assert start is not None
raise parse.ParseError("incomplete input", type, value, (prefix, start))
assert p.rootnode is not None
return p.rootnode
def parse_file(
self, filename: Path, encoding: str | None = None, debug: bool = False
) -> NL:
"""Parse a file and return the syntax tree."""
with open(filename, encoding=encoding) as stream:
text = stream.read()
return self.parse_string(text, debug)
def parse_string(self, text: str, debug: bool = False) -> NL:
"""Parse a string and return the syntax tree."""
tokens = tokenize.tokenize(text, grammar=self.grammar)
return self.parse_tokens(tokens, debug)
def _partially_consume_prefix(self, prefix: str, column: int) -> tuple[str, str]:
lines: list[str] = []
current_line = ""
current_column = 0
wait_for_nl = False
for char in prefix:
current_line += char
if wait_for_nl:
if char == "\n":
if current_line.strip() and current_column < column:
res = "".join(lines)
return res, prefix[len(res) :]
lines.append(current_line)
current_line = ""
current_column = 0
wait_for_nl = False
elif char in " \t":
current_column += 1
elif char == "\n":
# unexpected empty line
current_column = 0
elif char == "\f":
current_column = 0
else:
# indent is finished
wait_for_nl = True
return "".join(lines), current_line
def _generate_pickle_name(gt: Path, cache_dir: Path | None = None) -> str:
head, tail = os.path.splitext(gt)
if tail == ".txt":
tail = ""
name = head + tail + ".".join(map(str, sys.version_info)) + ".pickle"
if cache_dir:
return os.path.join(cache_dir, os.path.basename(name))
else:
return name
def load_grammar(
gt: str = "Grammar.txt",
gp: str | None = None,
save: bool = True,
force: bool = False,
logger: Logger | None = None,
) -> Grammar:
"""Load the grammar (maybe from a pickle)."""
if logger is None:
logger = logging.getLogger(__name__)
gp = _generate_pickle_name(gt) if gp is None else gp
if force or not _newer(gp, gt):
g: grammar.Grammar = pgen.generate_grammar(gt)
if save:
try:
g.dump(gp)
except OSError:
# Ignore error, caching is not vital.
pass
else:
g = grammar.Grammar()
g.load(gp)
return g
def _newer(a: str, b: str) -> bool:
"""Inquire whether file a was written since file b."""
if not os.path.exists(a):
return False
if not os.path.exists(b):
return True
return os.path.getmtime(a) >= os.path.getmtime(b)
def load_packaged_grammar(
package: str, grammar_source: str, cache_dir: Path | None = None
) -> grammar.Grammar:
"""Normally, loads a pickled grammar by doing
pkgutil.get_data(package, pickled_grammar)
where *pickled_grammar* is computed from *grammar_source* by adding the
Python version and using a ``.pickle`` extension.
However, if *grammar_source* is an extant file, load_grammar(grammar_source)
is called instead. This facilitates using a packaged grammar file when needed
but preserves load_grammar's automatic regeneration behavior when possible.
"""
if os.path.isfile(grammar_source):
gp = _generate_pickle_name(grammar_source, cache_dir) if cache_dir else None
return load_grammar(grammar_source, gp=gp)
pickled_name = _generate_pickle_name(os.path.basename(grammar_source), cache_dir)
data = pkgutil.get_data(package, pickled_name)
assert data is not None
g = grammar.Grammar()
g.loads(data)
return g
def main(*args: str) -> bool:
"""Main program, when run as a script: produce grammar pickle files.
Calls load_grammar for each argument, a path to a grammar text file.
"""
if not args:
args = tuple(sys.argv[1:])
logging.basicConfig(level=logging.INFO, stream=sys.stdout, format="%(message)s")
for gt in args:
load_grammar(gt, save=True, force=True)
return True
if __name__ == "__main__":
sys.exit(int(not main()))

View file

@ -0,0 +1,228 @@
# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
# Licensed to PSF under a Contributor Agreement.
"""This module defines the data structures used to represent a grammar.
These are a bit arcane because they are derived from the data
structures used by Python's 'pgen' parser generator.
There's also a table here mapping operators to their names in the
token module; the Python tokenize module reports all operators as the
fallback token code OP, but the parser needs the actual token code.
"""
# Python imports
import os
import pickle
import tempfile
from typing import Any, Optional, TypeVar, Union
# Local imports
from . import token
_P = TypeVar("_P", bound="Grammar")
Label = tuple[int, Optional[str]]
DFA = list[list[tuple[int, int]]]
DFAS = tuple[DFA, dict[int, int]]
Path = Union[str, "os.PathLike[str]"]
class Grammar:
"""Pgen parsing tables conversion class.
Once initialized, this class supplies the grammar tables for the
parsing engine implemented by parse.py. The parsing engine
accesses the instance variables directly. The class here does not
provide initialization of the tables; several subclasses exist to
do this (see the conv and pgen modules).
The load() method reads the tables from a pickle file, which is
much faster than the other ways offered by subclasses. The pickle
file is written by calling dump() (after loading the grammar
tables using a subclass). The report() method prints a readable
representation of the tables to stdout, for debugging.
The instance variables are as follows:
symbol2number -- a dict mapping symbol names to numbers. Symbol
numbers are always 256 or higher, to distinguish
them from token numbers, which are between 0 and
255 (inclusive).
number2symbol -- a dict mapping numbers to symbol names;
these two are each other's inverse.
states -- a list of DFAs, where each DFA is a list of
states, each state is a list of arcs, and each
arc is a (i, j) pair where i is a label and j is
a state number. The DFA number is the index into
this list. (This name is slightly confusing.)
Final states are represented by a special arc of
the form (0, j) where j is its own state number.
dfas -- a dict mapping symbol numbers to (DFA, first)
pairs, where DFA is an item from the states list
above, and first is a set of tokens that can
begin this grammar rule (represented by a dict
whose values are always 1).
labels -- a list of (x, y) pairs where x is either a token
number or a symbol number, and y is either None
or a string; the strings are keywords. The label
number is the index in this list; label numbers
are used to mark state transitions (arcs) in the
DFAs.
start -- the number of the grammar's start symbol.
keywords -- a dict mapping keyword strings to arc labels.
tokens -- a dict mapping token numbers to arc labels.
"""
def __init__(self) -> None:
self.symbol2number: dict[str, int] = {}
self.number2symbol: dict[int, str] = {}
self.states: list[DFA] = []
self.dfas: dict[int, DFAS] = {}
self.labels: list[Label] = [(0, "EMPTY")]
self.keywords: dict[str, int] = {}
self.soft_keywords: dict[str, int] = {}
self.tokens: dict[int, int] = {}
self.symbol2label: dict[str, int] = {}
self.version: tuple[int, int] = (0, 0)
self.start = 256
# Python 3.7+ parses async as a keyword, not an identifier
self.async_keywords = False
def dump(self, filename: Path) -> None:
"""Dump the grammar tables to a pickle file."""
# mypyc generates objects that don't have a __dict__, but they
# do have __getstate__ methods that will return an equivalent
# dictionary
if hasattr(self, "__dict__"):
d = self.__dict__
else:
d = self.__getstate__() # type: ignore
with tempfile.NamedTemporaryFile(
dir=os.path.dirname(filename), delete=False
) as f:
pickle.dump(d, f, pickle.HIGHEST_PROTOCOL)
os.replace(f.name, filename)
def _update(self, attrs: dict[str, Any]) -> None:
for k, v in attrs.items():
setattr(self, k, v)
def load(self, filename: Path) -> None:
"""Load the grammar tables from a pickle file."""
with open(filename, "rb") as f:
d = pickle.load(f)
self._update(d)
def loads(self, pkl: bytes) -> None:
"""Load the grammar tables from a pickle bytes object."""
self._update(pickle.loads(pkl))
def copy(self: _P) -> _P:
"""
Copy the grammar.
"""
new = self.__class__()
for dict_attr in (
"symbol2number",
"number2symbol",
"dfas",
"keywords",
"soft_keywords",
"tokens",
"symbol2label",
):
setattr(new, dict_attr, getattr(self, dict_attr).copy())
new.labels = self.labels[:]
new.states = self.states[:]
new.start = self.start
new.version = self.version
new.async_keywords = self.async_keywords
return new
def report(self) -> None:
"""Dump the grammar tables to standard output, for debugging."""
from pprint import pprint
print("s2n")
pprint(self.symbol2number)
print("n2s")
pprint(self.number2symbol)
print("states")
pprint(self.states)
print("dfas")
pprint(self.dfas)
print("labels")
pprint(self.labels)
print("start", self.start)
# Map from operator to number (since tokenize doesn't do this)
opmap_raw = """
( LPAR
) RPAR
[ LSQB
] RSQB
: COLON
, COMMA
; SEMI
+ PLUS
- MINUS
* STAR
/ SLASH
| VBAR
& AMPER
< LESS
> GREATER
= EQUAL
. DOT
% PERCENT
` BACKQUOTE
{ LBRACE
} RBRACE
@ AT
@= ATEQUAL
== EQEQUAL
!= NOTEQUAL
<> NOTEQUAL
<= LESSEQUAL
>= GREATEREQUAL
~ TILDE
^ CIRCUMFLEX
<< LEFTSHIFT
>> RIGHTSHIFT
** DOUBLESTAR
+= PLUSEQUAL
-= MINEQUAL
*= STAREQUAL
/= SLASHEQUAL
%= PERCENTEQUAL
&= AMPEREQUAL
|= VBAREQUAL
^= CIRCUMFLEXEQUAL
<<= LEFTSHIFTEQUAL
>>= RIGHTSHIFTEQUAL
**= DOUBLESTAREQUAL
// DOUBLESLASH
//= DOUBLESLASHEQUAL
-> RARROW
:= COLONEQUAL
! BANG
"""
opmap = {}
for line in opmap_raw.splitlines():
if line:
op, name = line.split()
opmap[op] = getattr(token, name)

View file

@ -0,0 +1,65 @@
# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
# Licensed to PSF under a Contributor Agreement.
"""Safely evaluate Python string literals without using eval()."""
import re
simple_escapes: dict[str, str] = {
"a": "\a",
"b": "\b",
"f": "\f",
"n": "\n",
"r": "\r",
"t": "\t",
"v": "\v",
"'": "'",
'"': '"',
"\\": "\\",
}
def escape(m: re.Match[str]) -> str:
all, tail = m.group(0, 1)
assert all.startswith("\\")
esc = simple_escapes.get(tail)
if esc is not None:
return esc
if tail.startswith("x"):
hexes = tail[1:]
if len(hexes) < 2:
raise ValueError(f"invalid hex string escape ('\\{tail}')")
try:
i = int(hexes, 16)
except ValueError:
raise ValueError(f"invalid hex string escape ('\\{tail}')") from None
else:
try:
i = int(tail, 8)
except ValueError:
raise ValueError(f"invalid octal string escape ('\\{tail}')") from None
return chr(i)
def evalString(s: str) -> str:
assert s.startswith("'") or s.startswith('"'), repr(s[:1])
q = s[0]
if s[:3] == q * 3:
q = q * 3
assert s.endswith(q), repr(s[-len(q) :])
assert len(s) >= 2 * len(q)
s = s[len(q) : -len(q)]
return re.sub(r"\\(\'|\"|\\|[abfnrtv]|x.{0,2}|[0-7]{1,3})", escape, s)
def test() -> None:
for i in range(256):
c = chr(i)
s = repr(c)
e = evalString(s)
if e != c:
print(i, c, s, e)
if __name__ == "__main__":
test()

View file

@ -0,0 +1,395 @@
# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
# Licensed to PSF under a Contributor Agreement.
"""Parser engine for the grammar tables generated by pgen.
The grammar table must be loaded first.
See Parser/parser.c in the Python distribution for additional info on
how this parsing engine works.
"""
from collections.abc import Callable, Iterator
from contextlib import contextmanager
from typing import TYPE_CHECKING, Union, cast
from blib2to3.pgen2.grammar import Grammar
from blib2to3.pytree import NL, Context, Leaf, Node, RawNode, convert
# Local imports
from . import grammar, token, tokenize
if TYPE_CHECKING:
from blib2to3.pgen2.driver import TokenProxy
Results = dict[str, NL]
Convert = Callable[[Grammar, RawNode], Union[Node, Leaf]]
DFA = list[list[tuple[int, int]]]
DFAS = tuple[DFA, dict[int, int]]
def lam_sub(grammar: Grammar, node: RawNode) -> NL:
assert node[3] is not None
return Node(type=node[0], children=node[3], context=node[2])
# A placeholder node, used when parser is backtracking.
DUMMY_NODE = (-1, None, None, None)
def stack_copy(
stack: list[tuple[DFAS, int, RawNode]],
) -> list[tuple[DFAS, int, RawNode]]:
"""Nodeless stack copy."""
return [(dfa, label, DUMMY_NODE) for dfa, label, _ in stack]
class Recorder:
def __init__(self, parser: "Parser", ilabels: list[int], context: Context) -> None:
self.parser = parser
self._ilabels = ilabels
self.context = context # not really matter
self._dead_ilabels: set[int] = set()
self._start_point = self.parser.stack
self._points = {ilabel: stack_copy(self._start_point) for ilabel in ilabels}
@property
def ilabels(self) -> set[int]:
return self._dead_ilabels.symmetric_difference(self._ilabels)
@contextmanager
def switch_to(self, ilabel: int) -> Iterator[None]:
with self.backtrack():
self.parser.stack = self._points[ilabel]
try:
yield
except ParseError:
self._dead_ilabels.add(ilabel)
finally:
self.parser.stack = self._start_point
@contextmanager
def backtrack(self) -> Iterator[None]:
"""
Use the node-level invariant ones for basic parsing operations (push/pop/shift).
These still will operate on the stack; but they won't create any new nodes, or
modify the contents of any other existing nodes.
This saves us a ton of time when we are backtracking, since we
want to restore to the initial state as quick as possible, which
can only be done by having as little mutatations as possible.
"""
is_backtracking = self.parser.is_backtracking
try:
self.parser.is_backtracking = True
yield
finally:
self.parser.is_backtracking = is_backtracking
def add_token(self, tok_type: int, tok_val: str, raw: bool = False) -> None:
for ilabel in self.ilabels:
with self.switch_to(ilabel):
if raw:
self.parser._addtoken(ilabel, tok_type, tok_val, self.context)
else:
self.parser.addtoken(tok_type, tok_val, self.context)
def determine_route(
self, value: str | None = None, force: bool = False
) -> int | None:
alive_ilabels = self.ilabels
if len(alive_ilabels) == 0:
*_, most_successful_ilabel = self._dead_ilabels
raise ParseError("bad input", most_successful_ilabel, value, self.context)
ilabel, *rest = alive_ilabels
if force or not rest:
return ilabel
else:
return None
class ParseError(Exception):
"""Exception to signal the parser is stuck."""
def __init__(
self, msg: str, type: int | None, value: str | None, context: Context
) -> None:
Exception.__init__(
self, f"{msg}: type={type!r}, value={value!r}, context={context!r}"
)
self.msg = msg
self.type = type
self.value = value
self.context = context
class Parser:
"""Parser engine.
The proper usage sequence is:
p = Parser(grammar, [converter]) # create instance
p.setup([start]) # prepare for parsing
<for each input token>:
if p.addtoken(...): # parse a token; may raise ParseError
break
root = p.rootnode # root of abstract syntax tree
A Parser instance may be reused by calling setup() repeatedly.
A Parser instance contains state pertaining to the current token
sequence, and should not be used concurrently by different threads
to parse separate token sequences.
See driver.py for how to get input tokens by tokenizing a file or
string.
Parsing is complete when addtoken() returns True; the root of the
abstract syntax tree can then be retrieved from the rootnode
instance variable. When a syntax error occurs, addtoken() raises
the ParseError exception. There is no error recovery; the parser
cannot be used after a syntax error was reported (but it can be
reinitialized by calling setup()).
"""
def __init__(self, grammar: Grammar, convert: Convert | None = None) -> None:
"""Constructor.
The grammar argument is a grammar.Grammar instance; see the
grammar module for more information.
The parser is not ready yet for parsing; you must call the
setup() method to get it started.
The optional convert argument is a function mapping concrete
syntax tree nodes to abstract syntax tree nodes. If not
given, no conversion is done and the syntax tree produced is
the concrete syntax tree. If given, it must be a function of
two arguments, the first being the grammar (a grammar.Grammar
instance), and the second being the concrete syntax tree node
to be converted. The syntax tree is converted from the bottom
up.
**post-note: the convert argument is ignored since for Black's
usage, convert will always be blib2to3.pytree.convert. Allowing
this to be dynamic hurts mypyc's ability to use early binding.
These docs are left for historical and informational value.
A concrete syntax tree node is a (type, value, context, nodes)
tuple, where type is the node type (a token or symbol number),
value is None for symbols and a string for tokens, context is
None or an opaque value used for error reporting (typically a
(lineno, offset) pair), and nodes is a list of children for
symbols, and None for tokens.
An abstract syntax tree node may be anything; this is entirely
up to the converter function.
"""
self.grammar = grammar
# See note in docstring above. TL;DR this is ignored.
self.convert = convert or lam_sub
self.is_backtracking = False
self.last_token: int | None = None
def setup(self, proxy: "TokenProxy", start: int | None = None) -> None:
"""Prepare for parsing.
This *must* be called before starting to parse.
The optional argument is an alternative start symbol; it
defaults to the grammar's start symbol.
You can use a Parser instance to parse any number of programs;
each time you call setup() the parser is reset to an initial
state determined by the (implicit or explicit) start symbol.
"""
if start is None:
start = self.grammar.start
# Each stack entry is a tuple: (dfa, state, node).
# A node is a tuple: (type, value, context, children),
# where children is a list of nodes or None, and context may be None.
newnode: RawNode = (start, None, None, [])
stackentry = (self.grammar.dfas[start], 0, newnode)
self.stack: list[tuple[DFAS, int, RawNode]] = [stackentry]
self.rootnode: NL | None = None
self.used_names: set[str] = set()
self.proxy = proxy
self.last_token = None
def addtoken(self, type: int, value: str, context: Context) -> bool:
"""Add a token; return True iff this is the end of the program."""
# Map from token to label
ilabels = self.classify(type, value, context)
assert len(ilabels) >= 1
# If we have only one state to advance, we'll directly
# take it as is.
if len(ilabels) == 1:
[ilabel] = ilabels
return self._addtoken(ilabel, type, value, context)
# If there are multiple states which we can advance (only
# happen under soft-keywords), then we will try all of them
# in parallel and as soon as one state can reach further than
# the rest, we'll choose that one. This is a pretty hacky
# and hopefully temporary algorithm.
#
# For a more detailed explanation, check out this post:
# https://tree.science/what-the-backtracking.html
with self.proxy.release() as proxy:
counter, force = 0, False
recorder = Recorder(self, ilabels, context)
recorder.add_token(type, value, raw=True)
next_token_value = value
while recorder.determine_route(next_token_value) is None:
if not proxy.can_advance(counter):
force = True
break
next_token_type, next_token_value, *_ = proxy.eat(counter)
if next_token_type in (tokenize.COMMENT, tokenize.NL):
counter += 1
continue
if next_token_type == tokenize.OP:
next_token_type = grammar.opmap[next_token_value]
recorder.add_token(next_token_type, next_token_value)
counter += 1
ilabel = cast(int, recorder.determine_route(next_token_value, force=force))
assert ilabel is not None
return self._addtoken(ilabel, type, value, context)
def _addtoken(self, ilabel: int, type: int, value: str, context: Context) -> bool:
# Loop until the token is shifted; may raise exceptions
while True:
dfa, state, node = self.stack[-1]
states, first = dfa
arcs = states[state]
# Look for a state with this label
for i, newstate in arcs:
t = self.grammar.labels[i][0]
if t >= 256:
# See if it's a symbol and if we're in its first set
itsdfa = self.grammar.dfas[t]
itsstates, itsfirst = itsdfa
if ilabel in itsfirst:
# Push a symbol
self.push(t, itsdfa, newstate, context)
break # To continue the outer while loop
elif ilabel == i:
# Look it up in the list of labels
# Shift a token; we're done with it
self.shift(type, value, newstate, context)
# Pop while we are in an accept-only state
state = newstate
while states[state] == [(0, state)]:
self.pop()
if not self.stack:
# Done parsing!
return True
dfa, state, node = self.stack[-1]
states, first = dfa
# Done with this token
self.last_token = type
return False
else:
if (0, state) in arcs:
# An accepting state, pop it and try something else
self.pop()
if not self.stack:
# Done parsing, but another token is input
raise ParseError("too much input", type, value, context)
else:
# No success finding a transition
raise ParseError("bad input", type, value, context)
def classify(self, type: int, value: str, context: Context) -> list[int]:
"""Turn a token into a label. (Internal)
Depending on whether the value is a soft-keyword or not,
this function may return multiple labels to choose from."""
if type == token.NAME:
# Keep a listing of all used names
self.used_names.add(value)
# Check for reserved words
if value in self.grammar.keywords:
return [self.grammar.keywords[value]]
elif value in self.grammar.soft_keywords:
assert type in self.grammar.tokens
# Current soft keywords (match, case, type) can only appear at the
# beginning of a statement. So as a shortcut, don't try to treat them
# like keywords in any other context.
# ('_' is also a soft keyword in the real grammar, but for our grammar
# it's just an expression, so we don't need to treat it specially.)
if self.last_token not in (
None,
token.INDENT,
token.DEDENT,
token.NEWLINE,
token.SEMI,
token.COLON,
):
return [self.grammar.tokens[type]]
return [
self.grammar.tokens[type],
self.grammar.soft_keywords[value],
]
ilabel = self.grammar.tokens.get(type)
if ilabel is None:
raise ParseError("bad token", type, value, context)
return [ilabel]
def shift(self, type: int, value: str, newstate: int, context: Context) -> None:
"""Shift a token. (Internal)"""
if self.is_backtracking:
dfa, state, _ = self.stack[-1]
self.stack[-1] = (dfa, newstate, DUMMY_NODE)
else:
dfa, state, node = self.stack[-1]
rawnode: RawNode = (type, value, context, None)
newnode = convert(self.grammar, rawnode)
assert node[-1] is not None
node[-1].append(newnode)
self.stack[-1] = (dfa, newstate, node)
def push(self, type: int, newdfa: DFAS, newstate: int, context: Context) -> None:
"""Push a nonterminal. (Internal)"""
if self.is_backtracking:
dfa, state, _ = self.stack[-1]
self.stack[-1] = (dfa, newstate, DUMMY_NODE)
self.stack.append((newdfa, 0, DUMMY_NODE))
else:
dfa, state, node = self.stack[-1]
newnode: RawNode = (type, None, context, [])
self.stack[-1] = (dfa, newstate, node)
self.stack.append((newdfa, 0, newnode))
def pop(self) -> None:
"""Pop a nonterminal. (Internal)"""
if self.is_backtracking:
self.stack.pop()
else:
popdfa, popstate, popnode = self.stack.pop()
newnode = convert(self.grammar, popnode)
if self.stack:
dfa, state, node = self.stack[-1]
assert node[-1] is not None
node[-1].append(newnode)
else:
self.rootnode = newnode
self.rootnode.used_names = self.used_names

View file

@ -0,0 +1,411 @@
# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
# Licensed to PSF under a Contributor Agreement.
import os
from collections.abc import Iterator, Sequence
from typing import IO, Any, NoReturn, Union
from blib2to3.pgen2 import grammar, token, tokenize
from blib2to3.pgen2.tokenize import TokenInfo
Path = Union[str, "os.PathLike[str]"]
class PgenGrammar(grammar.Grammar):
pass
class ParserGenerator:
filename: Path
stream: IO[str]
generator: Iterator[TokenInfo]
first: dict[str, dict[str, int] | None]
def __init__(self, filename: Path, stream: IO[str] | None = None) -> None:
close_stream = None
if stream is None:
stream = open(filename, encoding="utf-8")
close_stream = stream.close
self.filename = filename
self.generator = tokenize.tokenize(stream.read())
self.gettoken() # Initialize lookahead
self.dfas, self.startsymbol = self.parse()
if close_stream is not None:
close_stream()
self.first = {} # map from symbol name to set of tokens
self.addfirstsets()
def make_grammar(self) -> PgenGrammar:
c = PgenGrammar()
names = list(self.dfas.keys())
names.sort()
names.remove(self.startsymbol)
names.insert(0, self.startsymbol)
for name in names:
i = 256 + len(c.symbol2number)
c.symbol2number[name] = i
c.number2symbol[i] = name
for name in names:
dfa = self.dfas[name]
states = []
for state in dfa:
arcs = []
for label, next in sorted(state.arcs.items()):
arcs.append((self.make_label(c, label), dfa.index(next)))
if state.isfinal:
arcs.append((0, dfa.index(state)))
states.append(arcs)
c.states.append(states)
c.dfas[c.symbol2number[name]] = (states, self.make_first(c, name))
c.start = c.symbol2number[self.startsymbol]
return c
def make_first(self, c: PgenGrammar, name: str) -> dict[int, int]:
rawfirst = self.first[name]
assert rawfirst is not None
first = {}
for label in sorted(rawfirst):
ilabel = self.make_label(c, label)
##assert ilabel not in first # XXX failed on <> ... !=
first[ilabel] = 1
return first
def make_label(self, c: PgenGrammar, label: str) -> int:
# XXX Maybe this should be a method on a subclass of converter?
ilabel = len(c.labels)
if label[0].isalpha():
# Either a symbol name or a named token
if label in c.symbol2number:
# A symbol name (a non-terminal)
if label in c.symbol2label:
return c.symbol2label[label]
else:
c.labels.append((c.symbol2number[label], None))
c.symbol2label[label] = ilabel
return ilabel
else:
# A named token (NAME, NUMBER, STRING)
itoken = getattr(token, label, None)
assert isinstance(itoken, int), label
assert itoken in token.tok_name, label
if itoken in c.tokens:
return c.tokens[itoken]
else:
c.labels.append((itoken, None))
c.tokens[itoken] = ilabel
return ilabel
else:
# Either a keyword or an operator
assert label[0] in ('"', "'"), label
value = eval(label)
if value[0].isalpha():
if label[0] == '"':
keywords = c.soft_keywords
else:
keywords = c.keywords
# A keyword
if value in keywords:
return keywords[value]
else:
c.labels.append((token.NAME, value))
keywords[value] = ilabel
return ilabel
else:
# An operator (any non-numeric token)
itoken = grammar.opmap[value] # Fails if unknown token
if itoken in c.tokens:
return c.tokens[itoken]
else:
c.labels.append((itoken, None))
c.tokens[itoken] = ilabel
return ilabel
def addfirstsets(self) -> None:
names = list(self.dfas.keys())
names.sort()
for name in names:
if name not in self.first:
self.calcfirst(name)
# print name, self.first[name].keys()
def calcfirst(self, name: str) -> None:
dfa = self.dfas[name]
self.first[name] = None # dummy to detect left recursion
state = dfa[0]
totalset: dict[str, int] = {}
overlapcheck = {}
for label in state.arcs:
if label in self.dfas:
if label in self.first:
fset = self.first[label]
if fset is None:
raise ValueError(f"recursion for rule {name!r}")
else:
self.calcfirst(label)
fset = self.first[label]
assert fset is not None
totalset.update(fset)
overlapcheck[label] = fset
else:
totalset[label] = 1
overlapcheck[label] = {label: 1}
inverse: dict[str, str] = {}
for label, itsfirst in overlapcheck.items():
for symbol in itsfirst:
if symbol in inverse:
raise ValueError(
f"rule {name} is ambiguous; {symbol} is in the first sets of"
f" {label} as well as {inverse[symbol]}"
)
inverse[symbol] = label
self.first[name] = totalset
def parse(self) -> tuple[dict[str, list["DFAState"]], str]:
dfas = {}
startsymbol: str | None = None
# MSTART: (NEWLINE | RULE)* ENDMARKER
while self.type != token.ENDMARKER:
while self.type == token.NEWLINE:
self.gettoken()
# RULE: NAME ':' RHS NEWLINE
name = self.expect(token.NAME)
self.expect(token.OP, ":")
a, z = self.parse_rhs()
self.expect(token.NEWLINE)
# self.dump_nfa(name, a, z)
dfa = self.make_dfa(a, z)
# self.dump_dfa(name, dfa)
# oldlen = len(dfa)
self.simplify_dfa(dfa)
# newlen = len(dfa)
dfas[name] = dfa
# print name, oldlen, newlen
if startsymbol is None:
startsymbol = name
assert startsymbol is not None
return dfas, startsymbol
def make_dfa(self, start: "NFAState", finish: "NFAState") -> list["DFAState"]:
# To turn an NFA into a DFA, we define the states of the DFA
# to correspond to *sets* of states of the NFA. Then do some
# state reduction. Let's represent sets as dicts with 1 for
# values.
assert isinstance(start, NFAState)
assert isinstance(finish, NFAState)
def closure(state: NFAState) -> dict[NFAState, int]:
base: dict[NFAState, int] = {}
addclosure(state, base)
return base
def addclosure(state: NFAState, base: dict[NFAState, int]) -> None:
assert isinstance(state, NFAState)
if state in base:
return
base[state] = 1
for label, next in state.arcs:
if label is None:
addclosure(next, base)
states = [DFAState(closure(start), finish)]
for state in states: # NB states grows while we're iterating
arcs: dict[str, dict[NFAState, int]] = {}
for nfastate in state.nfaset:
for label, next in nfastate.arcs:
if label is not None:
addclosure(next, arcs.setdefault(label, {}))
for label, nfaset in sorted(arcs.items()):
for st in states:
if st.nfaset == nfaset:
break
else:
st = DFAState(nfaset, finish)
states.append(st)
state.addarc(st, label)
return states # List of DFAState instances; first one is start
def dump_nfa(self, name: str, start: "NFAState", finish: "NFAState") -> None:
print("Dump of NFA for", name)
todo = [start]
for i, state in enumerate(todo):
print(" State", i, state is finish and "(final)" or "")
for label, next in state.arcs:
if next in todo:
j = todo.index(next)
else:
j = len(todo)
todo.append(next)
if label is None:
print(f" -> {j}")
else:
print(f" {label} -> {j}")
def dump_dfa(self, name: str, dfa: Sequence["DFAState"]) -> None:
print("Dump of DFA for", name)
for i, state in enumerate(dfa):
print(" State", i, state.isfinal and "(final)" or "")
for label, next in sorted(state.arcs.items()):
print(f" {label} -> {dfa.index(next)}")
def simplify_dfa(self, dfa: list["DFAState"]) -> None:
# This is not theoretically optimal, but works well enough.
# Algorithm: repeatedly look for two states that have the same
# set of arcs (same labels pointing to the same nodes) and
# unify them, until things stop changing.
# dfa is a list of DFAState instances
changes = True
while changes:
changes = False
for i, state_i in enumerate(dfa):
for j in range(i + 1, len(dfa)):
state_j = dfa[j]
if state_i == state_j:
# print " unify", i, j
del dfa[j]
for state in dfa:
state.unifystate(state_j, state_i)
changes = True
break
def parse_rhs(self) -> tuple["NFAState", "NFAState"]:
# RHS: ALT ('|' ALT)*
a, z = self.parse_alt()
if self.value != "|":
return a, z
else:
aa = NFAState()
zz = NFAState()
aa.addarc(a)
z.addarc(zz)
while self.value == "|":
self.gettoken()
a, z = self.parse_alt()
aa.addarc(a)
z.addarc(zz)
return aa, zz
def parse_alt(self) -> tuple["NFAState", "NFAState"]:
# ALT: ITEM+
a, b = self.parse_item()
while self.value in ("(", "[") or self.type in (token.NAME, token.STRING):
c, d = self.parse_item()
b.addarc(c)
b = d
return a, b
def parse_item(self) -> tuple["NFAState", "NFAState"]:
# ITEM: '[' RHS ']' | ATOM ['+' | '*']
if self.value == "[":
self.gettoken()
a, z = self.parse_rhs()
self.expect(token.OP, "]")
a.addarc(z)
return a, z
else:
a, z = self.parse_atom()
value = self.value
if value not in ("+", "*"):
return a, z
self.gettoken()
z.addarc(a)
if value == "+":
return a, z
else:
return a, a
def parse_atom(self) -> tuple["NFAState", "NFAState"]:
# ATOM: '(' RHS ')' | NAME | STRING
if self.value == "(":
self.gettoken()
a, z = self.parse_rhs()
self.expect(token.OP, ")")
return a, z
elif self.type in (token.NAME, token.STRING):
a = NFAState()
z = NFAState()
a.addarc(z, self.value)
self.gettoken()
return a, z
else:
self.raise_error(
f"expected (...) or NAME or STRING, got {self.type}/{self.value}"
)
def expect(self, type: int, value: Any | None = None) -> str:
if self.type != type or (value is not None and self.value != value):
self.raise_error(f"expected {type}/{value}, got {self.type}/{self.value}")
value = self.value
self.gettoken()
return value
def gettoken(self) -> None:
tup = next(self.generator)
while tup[0] in (tokenize.COMMENT, tokenize.NL):
tup = next(self.generator)
self.type, self.value, self.begin, self.end, self.line = tup
# print token.tok_name[self.type], repr(self.value)
def raise_error(self, msg: str) -> NoReturn:
raise SyntaxError(
msg, (str(self.filename), self.end[0], self.end[1], self.line)
)
class NFAState:
arcs: list[tuple[str | None, "NFAState"]]
def __init__(self) -> None:
self.arcs = [] # list of (label, NFAState) pairs
def addarc(self, next: "NFAState", label: str | None = None) -> None:
assert label is None or isinstance(label, str)
assert isinstance(next, NFAState)
self.arcs.append((label, next))
class DFAState:
nfaset: dict[NFAState, Any]
isfinal: bool
arcs: dict[str, "DFAState"]
def __init__(self, nfaset: dict[NFAState, Any], final: NFAState) -> None:
assert isinstance(nfaset, dict)
assert isinstance(next(iter(nfaset)), NFAState)
assert isinstance(final, NFAState)
self.nfaset = nfaset
self.isfinal = final in nfaset
self.arcs = {} # map from label to DFAState
def addarc(self, next: "DFAState", label: str) -> None:
assert isinstance(label, str)
assert label not in self.arcs
assert isinstance(next, DFAState)
self.arcs[label] = next
def unifystate(self, old: "DFAState", new: "DFAState") -> None:
for label, next in self.arcs.items():
if next is old:
self.arcs[label] = new
def __eq__(self, other: Any) -> bool:
# Equality test -- ignore the nfaset instance variable
assert isinstance(other, DFAState)
if self.isfinal != other.isfinal:
return False
# Can't just return self.arcs == other.arcs, because that
# would invoke this method recursively, with cycles...
if len(self.arcs) != len(other.arcs):
return False
for label, next in self.arcs.items():
if next is not other.arcs.get(label):
return False
return True
__hash__: Any = None # For Py3 compatibility.
def generate_grammar(filename: Path = "Grammar.txt") -> PgenGrammar:
p = ParserGenerator(filename)
return p.make_grammar()

View file

@ -0,0 +1,95 @@
"""Token constants (from "token.h")."""
from typing import Final
# Taken from Python (r53757) and modified to include some tokens
# originally monkeypatched in by pgen2.tokenize
# --start constants--
ENDMARKER: Final = 0
NAME: Final = 1
NUMBER: Final = 2
STRING: Final = 3
NEWLINE: Final = 4
INDENT: Final = 5
DEDENT: Final = 6
LPAR: Final = 7
RPAR: Final = 8
LSQB: Final = 9
RSQB: Final = 10
COLON: Final = 11
COMMA: Final = 12
SEMI: Final = 13
PLUS: Final = 14
MINUS: Final = 15
STAR: Final = 16
SLASH: Final = 17
VBAR: Final = 18
AMPER: Final = 19
LESS: Final = 20
GREATER: Final = 21
EQUAL: Final = 22
DOT: Final = 23
PERCENT: Final = 24
BACKQUOTE: Final = 25
LBRACE: Final = 26
RBRACE: Final = 27
EQEQUAL: Final = 28
NOTEQUAL: Final = 29
LESSEQUAL: Final = 30
GREATEREQUAL: Final = 31
TILDE: Final = 32
CIRCUMFLEX: Final = 33
LEFTSHIFT: Final = 34
RIGHTSHIFT: Final = 35
DOUBLESTAR: Final = 36
PLUSEQUAL: Final = 37
MINEQUAL: Final = 38
STAREQUAL: Final = 39
SLASHEQUAL: Final = 40
PERCENTEQUAL: Final = 41
AMPEREQUAL: Final = 42
VBAREQUAL: Final = 43
CIRCUMFLEXEQUAL: Final = 44
LEFTSHIFTEQUAL: Final = 45
RIGHTSHIFTEQUAL: Final = 46
DOUBLESTAREQUAL: Final = 47
DOUBLESLASH: Final = 48
DOUBLESLASHEQUAL: Final = 49
AT: Final = 50
ATEQUAL: Final = 51
OP: Final = 52
COMMENT: Final = 53
NL: Final = 54
RARROW: Final = 55
AWAIT: Final = 56
ASYNC: Final = 57
ERRORTOKEN: Final = 58
COLONEQUAL: Final = 59
FSTRING_START: Final = 60
FSTRING_MIDDLE: Final = 61
FSTRING_END: Final = 62
BANG: Final = 63
TSTRING_START: Final = 64
TSTRING_MIDDLE: Final = 65
TSTRING_END: Final = 66
N_TOKENS: Final = 67
NT_OFFSET: Final = 256
# --end constants--
tok_name: Final[dict[int, str]] = {}
for _name, _value in list(globals().items()):
if type(_value) is int:
tok_name[_value] = _name
def ISTERMINAL(x: int) -> bool:
return x < NT_OFFSET
def ISNONTERMINAL(x: int) -> bool:
return x >= NT_OFFSET
def ISEOF(x: int) -> bool:
return x == ENDMARKER

View file

@ -0,0 +1,226 @@
# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation.
# All rights reserved.
# mypy: allow-untyped-defs, allow-untyped-calls
"""Tokenization help for Python programs.
generate_tokens(readline) is a generator that breaks a stream of
text into Python tokens. It accepts a readline-like method which is called
repeatedly to get the next line of input (or "" for EOF). It generates
5-tuples with these members:
the token type (see token.py)
the token (a string)
the starting (row, column) indices of the token (a 2-tuple of ints)
the ending (row, column) indices of the token (a 2-tuple of ints)
the original line (string)
It is designed to match the working of the Python tokenizer exactly, except
that it produces COMMENT tokens for comments and gives type OP for all
operators
Older entry points
tokenize_loop(readline, tokeneater)
tokenize(readline, tokeneater=printtoken)
are the same, except instead of generating tokens, tokeneater is a callback
function to which the 5 fields described above are passed as 5 arguments,
each time a new token is found."""
import sys
from collections.abc import Iterator
from blib2to3.pgen2.grammar import Grammar
from blib2to3.pgen2.token import (
ASYNC,
AWAIT,
COMMENT,
DEDENT,
ENDMARKER,
FSTRING_END,
FSTRING_MIDDLE,
FSTRING_START,
INDENT,
NAME,
NEWLINE,
NL,
NUMBER,
OP,
STRING,
TSTRING_END,
TSTRING_MIDDLE,
TSTRING_START,
tok_name,
)
__author__ = "Ka-Ping Yee <ping@lfw.org>"
__credits__ = "GvR, ESR, Tim Peters, Thomas Wouters, Fred Drake, Skip Montanaro"
import pytokens
from pytokens import TokenType
from . import token as _token
__all__ = [x for x in dir(_token) if x[0] != "_"] + [
"tokenize",
"generate_tokens",
"untokenize",
]
del _token
Coord = tuple[int, int]
TokenInfo = tuple[int, str, Coord, Coord, str]
TOKEN_TYPE_MAP = {
TokenType.indent: INDENT,
TokenType.dedent: DEDENT,
TokenType.newline: NEWLINE,
TokenType.nl: NL,
TokenType.comment: COMMENT,
TokenType.semicolon: OP,
TokenType.lparen: OP,
TokenType.rparen: OP,
TokenType.lbracket: OP,
TokenType.rbracket: OP,
TokenType.lbrace: OP,
TokenType.rbrace: OP,
TokenType.colon: OP,
TokenType.op: OP,
TokenType.identifier: NAME,
TokenType.number: NUMBER,
TokenType.string: STRING,
TokenType.fstring_start: FSTRING_START,
TokenType.fstring_middle: FSTRING_MIDDLE,
TokenType.fstring_end: FSTRING_END,
TokenType.tstring_start: TSTRING_START,
TokenType.tstring_middle: TSTRING_MIDDLE,
TokenType.tstring_end: TSTRING_END,
TokenType.endmarker: ENDMARKER,
}
class TokenError(Exception): ...
def transform_whitespace(
token: pytokens.Token, source: str, prev_token: pytokens.Token | None
) -> pytokens.Token:
r"""
Black treats `\\\n` at the end of a line as a 'NL' token, while it
is ignored as whitespace in the regular Python parser.
But, only the first one. If there's a `\\\n` following it
(as in, a \ just by itself on a line), that is not made into NL.
"""
if (
token.type == TokenType.whitespace
and prev_token is not None
and prev_token.type not in (TokenType.nl, TokenType.newline)
):
token_str = source[token.start_index : token.end_index]
if token_str.startswith("\\\r\n"):
return pytokens.Token(
TokenType.nl,
token.start_index,
token.start_index + 3,
token.start_line,
token.start_col,
token.start_line,
token.start_col + 3,
)
elif token_str.startswith("\\\n") or token_str.startswith("\\\r"):
return pytokens.Token(
TokenType.nl,
token.start_index,
token.start_index + 2,
token.start_line,
token.start_col,
token.start_line,
token.start_col + 2,
)
return token
def tokenize(source: str, grammar: Grammar | None = None) -> Iterator[TokenInfo]:
lines = source.split("\n")
lines += [""] # For newline tokens in files that don't end in a newline
line, column = 1, 0
prev_token: pytokens.Token | None = None
try:
for token in pytokens.tokenize(source):
token = transform_whitespace(token, source, prev_token)
line, column = token.start_line, token.start_col
if token.type == TokenType.whitespace:
continue
token_str = source[token.start_index : token.end_index]
if token.type == TokenType.newline and token_str == "":
# Black doesn't yield empty newline tokens at the end of a file
# if there's no newline at the end of a file.
prev_token = token
continue
source_line = lines[token.start_line - 1]
if token.type == TokenType.identifier and token_str in ("async", "await"):
# Black uses `async` and `await` token types just for those two keywords
yield (
ASYNC if token_str == "async" else AWAIT,
token_str,
(token.start_line, token.start_col),
(token.end_line, token.end_col),
source_line,
)
elif token.type == TokenType.op and token_str == "...":
# Black doesn't have an ellipsis token yet, yield 3 DOTs instead
assert token.start_line == token.end_line
assert token.end_col == token.start_col + 3
token_str = "."
for start_col in range(token.start_col, token.start_col + 3):
end_col = start_col + 1
yield (
TOKEN_TYPE_MAP[token.type],
token_str,
(token.start_line, start_col),
(token.end_line, end_col),
source_line,
)
else:
token_type = TOKEN_TYPE_MAP.get(token.type)
if token_type is None:
raise ValueError(f"Unknown token type: {token.type!r}")
yield (
TOKEN_TYPE_MAP[token.type],
token_str,
(token.start_line, token.start_col),
(token.end_line, token.end_col),
source_line,
)
prev_token = token
except pytokens.UnexpectedEOF:
raise TokenError("Unexpected EOF in multi-line statement", (line, column))
except pytokens.TokenizeError as exc:
raise TokenError(f"Failed to parse: {type(exc).__name__}", (line, column))
def printtoken(
type: int, token: str, srow_col: Coord, erow_col: Coord, line: str
) -> None: # for testing
srow, scol = srow_col
erow, ecol = erow_col
print(f"{srow},{scol}-{erow},{ecol}:\t{tok_name[type]}\t{token!r}")
if __name__ == "__main__": # testing
if len(sys.argv) > 1:
token_iterator = tokenize(open(sys.argv[1]).read())
else:
token_iterator = tokenize(sys.stdin.read())
for tok in token_iterator:
printtoken(*tok)

View file

@ -0,0 +1,208 @@
# Copyright 2006 Google, Inc. All Rights Reserved.
# Licensed to PSF under a Contributor Agreement.
"""Export the Python grammar and symbols."""
# Python imports
import os
from typing import Union
# Local imports
from .pgen2 import driver
from .pgen2.grammar import Grammar
# Moved into initialize because mypyc can't handle __file__ (XXX bug)
# # The grammar file
# _GRAMMAR_FILE = os.path.join(os.path.dirname(__file__), "Grammar.txt")
# _PATTERN_GRAMMAR_FILE = os.path.join(os.path.dirname(__file__),
# "PatternGrammar.txt")
class Symbols:
def __init__(self, grammar: Grammar) -> None:
"""Initializer.
Creates an attribute for each grammar symbol (nonterminal),
whose value is the symbol's type (an int >= 256).
"""
for name, symbol in grammar.symbol2number.items():
setattr(self, name, symbol)
class _python_symbols(Symbols):
and_expr: int
and_test: int
annassign: int
arglist: int
argument: int
arith_expr: int
asexpr_test: int
assert_stmt: int
async_funcdef: int
async_stmt: int
atom: int
augassign: int
break_stmt: int
case_block: int
classdef: int
comp_for: int
comp_if: int
comp_iter: int
comp_op: int
comparison: int
compound_stmt: int
continue_stmt: int
decorated: int
decorator: int
decorators: int
del_stmt: int
dictsetmaker: int
dotted_as_name: int
dotted_as_names: int
dotted_name: int
encoding_decl: int
eval_input: int
except_clause: int
expr: int
expr_stmt: int
exprlist: int
factor: int
file_input: int
flow_stmt: int
for_stmt: int
fstring: int
fstring_format_spec: int
fstring_middle: int
fstring_replacement_field: int
funcdef: int
global_stmt: int
guard: int
if_stmt: int
import_as_name: int
import_as_names: int
import_from: int
import_name: int
import_stmt: int
lambdef: int
listmaker: int
match_stmt: int
namedexpr_test: int
not_test: int
old_comp_for: int
old_comp_if: int
old_comp_iter: int
old_lambdef: int
old_test: int
or_test: int
parameters: int
paramspec: int
pass_stmt: int
pattern: int
patterns: int
power: int
raise_stmt: int
return_stmt: int
shift_expr: int
simple_stmt: int
single_input: int
sliceop: int
small_stmt: int
subject_expr: int
star_expr: int
stmt: int
subscript: int
subscriptlist: int
suite: int
term: int
test: int
testlist: int
testlist1: int
testlist_gexp: int
testlist_safe: int
testlist_star_expr: int
tfpdef: int
tfplist: int
tname: int
tname_star: int
trailer: int
try_stmt: int
tstring: int
tstring_format_spec: int
tstring_middle: int
tstring_replacement_field: int
type_stmt: int
typedargslist: int
typeparam: int
typeparams: int
typevar: int
typevartuple: int
varargslist: int
vfpdef: int
vfplist: int
vname: int
while_stmt: int
with_stmt: int
xor_expr: int
yield_arg: int
yield_expr: int
yield_stmt: int
class _pattern_symbols(Symbols):
Alternative: int
Alternatives: int
Details: int
Matcher: int
NegatedUnit: int
Repeater: int
Unit: int
python_grammar: Grammar
python_grammar_async_keywords: Grammar
python_grammar_soft_keywords: Grammar
pattern_grammar: Grammar
python_symbols: _python_symbols
pattern_symbols: _pattern_symbols
def initialize(cache_dir: Union[str, "os.PathLike[str]", None] = None) -> None:
global python_grammar
global python_grammar_async_keywords
global python_grammar_soft_keywords
global python_symbols
global pattern_grammar
global pattern_symbols
# The grammar file
_GRAMMAR_FILE = os.path.join(os.path.dirname(__file__), "Grammar.txt")
_PATTERN_GRAMMAR_FILE = os.path.join(
os.path.dirname(__file__), "PatternGrammar.txt"
)
python_grammar = driver.load_packaged_grammar("blib2to3", _GRAMMAR_FILE, cache_dir)
assert "print" not in python_grammar.keywords
assert "exec" not in python_grammar.keywords
soft_keywords = python_grammar.soft_keywords.copy()
python_grammar.soft_keywords.clear()
python_symbols = _python_symbols(python_grammar)
# Python 3.0-3.6
python_grammar.version = (3, 0)
# Python 3.7+
python_grammar_async_keywords = python_grammar.copy()
python_grammar_async_keywords.async_keywords = True
python_grammar_async_keywords.version = (3, 7)
# Python 3.10+
python_grammar_soft_keywords = python_grammar_async_keywords.copy()
python_grammar_soft_keywords.soft_keywords = soft_keywords
python_grammar_soft_keywords.version = (3, 10)
pattern_grammar = driver.load_packaged_grammar(
"blib2to3", _PATTERN_GRAMMAR_FILE, cache_dir
)
pattern_symbols = _pattern_symbols(pattern_grammar)

View file

@ -0,0 +1,971 @@
# Copyright 2006 Google, Inc. All Rights Reserved.
# Licensed to PSF under a Contributor Agreement.
"""
Python parse tree definitions.
This is a very concrete parse tree; we need to keep every token and
even the comments and whitespace between tokens.
There's also a pattern matching implementation here.
"""
# mypy: allow-untyped-defs, allow-incomplete-defs
from collections.abc import Iterable, Iterator
from typing import Any, Optional, TypeVar, Union
from blib2to3.pgen2.grammar import Grammar
__author__ = "Guido van Rossum <guido@python.org>"
import sys
from io import StringIO
HUGE: int = 0x7FFFFFFF # maximum repeat count, default max
_type_reprs: dict[int, str | int] = {}
def type_repr(type_num: int) -> str | int:
global _type_reprs
if not _type_reprs:
from . import pygram
if not hasattr(pygram, "python_symbols"):
pygram.initialize(cache_dir=None)
# printing tokens is possible but not as useful
# from .pgen2 import token // token.__dict__.items():
for name in dir(pygram.python_symbols):
val = getattr(pygram.python_symbols, name)
if type(val) == int:
_type_reprs[val] = name
return _type_reprs.setdefault(type_num, type_num)
_P = TypeVar("_P", bound="Base")
NL = Union["Node", "Leaf"]
Context = tuple[str, tuple[int, int]]
RawNode = tuple[int, Optional[str], Optional[Context], Optional[list[NL]]]
class Base:
"""
Abstract base class for Node and Leaf.
This provides some default functionality and boilerplate using the
template pattern.
A node may be a subnode of at most one parent.
"""
# Default values for instance variables
type: int # int: token number (< 256) or symbol number (>= 256)
parent: Optional["Node"] = None # Parent node pointer, or None
children: list[NL] # List of subnodes
was_changed: bool = False
was_checked: bool = False
def __new__(cls, *args, **kwds):
"""Constructor that prevents Base from being instantiated."""
assert cls is not Base, "Cannot instantiate Base"
return object.__new__(cls)
def __eq__(self, other: Any) -> bool:
"""
Compare two nodes for equality.
This calls the method _eq().
"""
if self.__class__ is not other.__class__:
return NotImplemented
return self._eq(other)
@property
def prefix(self) -> str:
raise NotImplementedError
def _eq(self: _P, other: _P) -> bool:
"""
Compare two nodes for equality.
This is called by __eq__ and __ne__. It is only called if the two nodes
have the same type. This must be implemented by the concrete subclass.
Nodes should be considered equal if they have the same structure,
ignoring the prefix string and other context information.
"""
raise NotImplementedError
def __deepcopy__(self: _P, memo: Any) -> _P:
return self.clone()
def clone(self: _P) -> _P:
"""
Return a cloned (deep) copy of self.
This must be implemented by the concrete subclass.
"""
raise NotImplementedError
def post_order(self) -> Iterator[NL]:
"""
Return a post-order iterator for the tree.
This must be implemented by the concrete subclass.
"""
raise NotImplementedError
def pre_order(self) -> Iterator[NL]:
"""
Return a pre-order iterator for the tree.
This must be implemented by the concrete subclass.
"""
raise NotImplementedError
def replace(self, new: NL | list[NL]) -> None:
"""Replace this node with a new one in the parent."""
assert self.parent is not None, str(self)
assert new is not None
if not isinstance(new, list):
new = [new]
l_children = []
found = False
for ch in self.parent.children:
if ch is self:
assert not found, (self.parent.children, self, new)
if new is not None:
l_children.extend(new)
found = True
else:
l_children.append(ch)
assert found, (self.children, self, new)
self.parent.children = l_children
self.parent.changed()
self.parent.invalidate_sibling_maps()
for x in new:
x.parent = self.parent
self.parent = None
def get_lineno(self) -> int | None:
"""Return the line number which generated the invocant node."""
node = self
while not isinstance(node, Leaf):
if not node.children:
return None
node = node.children[0]
return node.lineno
def changed(self) -> None:
if self.was_changed:
return
if self.parent:
self.parent.changed()
self.was_changed = True
def remove(self) -> int | None:
"""
Remove the node from the tree. Returns the position of the node in its
parent's children before it was removed.
"""
if self.parent:
for i, node in enumerate(self.parent.children):
if node is self:
del self.parent.children[i]
self.parent.changed()
self.parent.invalidate_sibling_maps()
self.parent = None
return i
return None
@property
def next_sibling(self) -> NL | None:
"""
The node immediately following the invocant in their parent's children
list. If the invocant does not have a next sibling, it is None
"""
if self.parent is None:
return None
if self.parent.next_sibling_map is None:
self.parent.update_sibling_maps()
assert self.parent.next_sibling_map is not None
return self.parent.next_sibling_map[id(self)]
@property
def prev_sibling(self) -> NL | None:
"""
The node immediately preceding the invocant in their parent's children
list. If the invocant does not have a previous sibling, it is None.
"""
if self.parent is None:
return None
if self.parent.prev_sibling_map is None:
self.parent.update_sibling_maps()
assert self.parent.prev_sibling_map is not None
return self.parent.prev_sibling_map[id(self)]
def leaves(self) -> Iterator["Leaf"]:
for child in self.children:
yield from child.leaves()
def depth(self) -> int:
if self.parent is None:
return 0
return 1 + self.parent.depth()
def get_suffix(self) -> str:
"""
Return the string immediately following the invocant node. This is
effectively equivalent to node.next_sibling.prefix
"""
next_sib = self.next_sibling
if next_sib is None:
return ""
prefix = next_sib.prefix
return prefix
class Node(Base):
"""Concrete implementation for interior nodes."""
fixers_applied: list[Any] | None
used_names: set[str] | None
def __init__(
self,
type: int,
children: list[NL],
context: Any | None = None,
prefix: str | None = None,
fixers_applied: list[Any] | None = None,
) -> None:
"""
Initializer.
Takes a type constant (a symbol number >= 256), a sequence of
child nodes, and an optional context keyword argument.
As a side effect, the parent pointers of the children are updated.
"""
assert type >= 256, type
self.type = type
self.children = list(children)
for ch in self.children:
assert ch.parent is None, repr(ch)
ch.parent = self
self.invalidate_sibling_maps()
if prefix is not None:
self.prefix = prefix
if fixers_applied:
self.fixers_applied = fixers_applied[:]
else:
self.fixers_applied = None
def __repr__(self) -> str:
"""Return a canonical string representation."""
assert self.type is not None
return f"{self.__class__.__name__}({type_repr(self.type)}, {self.children!r})"
def __str__(self) -> str:
"""
Return a pretty string representation.
This reproduces the input source exactly.
"""
return "".join(map(str, self.children))
def _eq(self, other: Base) -> bool:
"""Compare two nodes for equality."""
return (self.type, self.children) == (other.type, other.children)
def clone(self) -> "Node":
assert self.type is not None
"""Return a cloned (deep) copy of self."""
return Node(
self.type,
[ch.clone() for ch in self.children],
fixers_applied=self.fixers_applied,
)
def post_order(self) -> Iterator[NL]:
"""Return a post-order iterator for the tree."""
for child in self.children:
yield from child.post_order()
yield self
def pre_order(self) -> Iterator[NL]:
"""Return a pre-order iterator for the tree."""
yield self
for child in self.children:
yield from child.pre_order()
@property
def prefix(self) -> str:
"""
The whitespace and comments preceding this node in the input.
"""
if not self.children:
return ""
return self.children[0].prefix
@prefix.setter
def prefix(self, prefix: str) -> None:
if self.children:
self.children[0].prefix = prefix
def set_child(self, i: int, child: NL) -> None:
"""
Equivalent to 'node.children[i] = child'. This method also sets the
child's parent attribute appropriately.
"""
child.parent = self
self.children[i].parent = None
self.children[i] = child
self.changed()
self.invalidate_sibling_maps()
def insert_child(self, i: int, child: NL) -> None:
"""
Equivalent to 'node.children.insert(i, child)'. This method also sets
the child's parent attribute appropriately.
"""
child.parent = self
self.children.insert(i, child)
self.changed()
self.invalidate_sibling_maps()
def append_child(self, child: NL) -> None:
"""
Equivalent to 'node.children.append(child)'. This method also sets the
child's parent attribute appropriately.
"""
child.parent = self
self.children.append(child)
self.changed()
self.invalidate_sibling_maps()
def invalidate_sibling_maps(self) -> None:
self.prev_sibling_map: dict[int, NL | None] | None = None
self.next_sibling_map: dict[int, NL | None] | None = None
def update_sibling_maps(self) -> None:
_prev: dict[int, NL | None] = {}
_next: dict[int, NL | None] = {}
self.prev_sibling_map = _prev
self.next_sibling_map = _next
previous: NL | None = None
for current in self.children:
_prev[id(current)] = previous
_next[id(previous)] = current
previous = current
_next[id(current)] = None
class Leaf(Base):
"""Concrete implementation for leaf nodes."""
# Default values for instance variables
value: str
fixers_applied: list[Any]
bracket_depth: int
# Changed later in brackets.py
opening_bracket: Optional["Leaf"] = None
used_names: set[str] | None
_prefix = "" # Whitespace and comments preceding this token in the input
lineno: int = 0 # Line where this token starts in the input
column: int = 0 # Column where this token starts in the input
# If not None, this Leaf is created by converting a block of fmt off/skip
# code, and `fmt_pass_converted_first_leaf` points to the first Leaf in the
# converted code.
fmt_pass_converted_first_leaf: Optional["Leaf"] = None
def __init__(
self,
type: int,
value: str,
context: Context | None = None,
prefix: str | None = None,
fixers_applied: list[Any] = [],
opening_bracket: Optional["Leaf"] = None,
fmt_pass_converted_first_leaf: Optional["Leaf"] = None,
) -> None:
"""
Initializer.
Takes a type constant (a token number < 256), a string value, and an
optional context keyword argument.
"""
assert 0 <= type < 256, type
if context is not None:
self._prefix, (self.lineno, self.column) = context
self.type = type
self.value = value
if prefix is not None:
self._prefix = prefix
self.fixers_applied: list[Any] | None = fixers_applied[:]
self.children = []
self.opening_bracket = opening_bracket
self.fmt_pass_converted_first_leaf = fmt_pass_converted_first_leaf
def __repr__(self) -> str:
"""Return a canonical string representation."""
from .pgen2.token import tok_name
assert self.type is not None
return (
f"{self.__class__.__name__}({tok_name.get(self.type, self.type)},"
f" {self.value!r})"
)
def __str__(self) -> str:
"""
Return a pretty string representation.
This reproduces the input source exactly.
"""
return self._prefix + str(self.value)
def _eq(self, other: "Leaf") -> bool:
"""Compare two nodes for equality."""
return (self.type, self.value) == (other.type, other.value)
def clone(self) -> "Leaf":
assert self.type is not None
"""Return a cloned (deep) copy of self."""
return Leaf(
self.type,
self.value,
(self.prefix, (self.lineno, self.column)),
fixers_applied=self.fixers_applied,
)
def leaves(self) -> Iterator["Leaf"]:
yield self
def post_order(self) -> Iterator["Leaf"]:
"""Return a post-order iterator for the tree."""
yield self
def pre_order(self) -> Iterator["Leaf"]:
"""Return a pre-order iterator for the tree."""
yield self
@property
def prefix(self) -> str:
"""
The whitespace and comments preceding this token in the input.
"""
return self._prefix
@prefix.setter
def prefix(self, prefix: str) -> None:
self.changed()
self._prefix = prefix
def convert(gr: Grammar, raw_node: RawNode) -> NL:
"""
Convert raw node information to a Node or Leaf instance.
This is passed to the parser driver which calls it whenever a reduction of a
grammar rule produces a new complete node, so that the tree is build
strictly bottom-up.
"""
type, value, context, children = raw_node
if children or type in gr.number2symbol:
# If there's exactly one child, return that child instead of
# creating a new node.
assert children is not None
if len(children) == 1:
return children[0]
return Node(type, children, context=context)
else:
return Leaf(type, value or "", context=context)
_Results = dict[str, NL]
class BasePattern:
"""
A pattern is a tree matching pattern.
It looks for a specific node type (token or symbol), and
optionally for a specific content.
This is an abstract base class. There are three concrete
subclasses:
- LeafPattern matches a single leaf node;
- NodePattern matches a single node (usually non-leaf);
- WildcardPattern matches a sequence of nodes of variable length.
"""
# Defaults for instance variables
type: int | None
type = None # Node type (token if < 256, symbol if >= 256)
content: Any = None # Optional content matching pattern
name: str | None = None # Optional name used to store match in results dict
def __new__(cls, *args, **kwds):
"""Constructor that prevents BasePattern from being instantiated."""
assert cls is not BasePattern, "Cannot instantiate BasePattern"
return object.__new__(cls)
def __repr__(self) -> str:
assert self.type is not None
args = [type_repr(self.type), self.content, self.name]
while args and args[-1] is None:
del args[-1]
return f"{self.__class__.__name__}({', '.join(map(repr, args))})"
def _submatch(self, node, results=None) -> bool:
raise NotImplementedError
def optimize(self) -> "BasePattern":
"""
A subclass can define this as a hook for optimizations.
Returns either self or another node with the same effect.
"""
return self
def match(self, node: NL, results: _Results | None = None) -> bool:
"""
Does this pattern exactly match a node?
Returns True if it matches, False if not.
If results is not None, it must be a dict which will be
updated with the nodes matching named subpatterns.
Default implementation for non-wildcard patterns.
"""
if self.type is not None and node.type != self.type:
return False
if self.content is not None:
r: _Results | None = None
if results is not None:
r = {}
if not self._submatch(node, r):
return False
if r:
assert results is not None
results.update(r)
if results is not None and self.name:
results[self.name] = node
return True
def match_seq(self, nodes: list[NL], results: _Results | None = None) -> bool:
"""
Does this pattern exactly match a sequence of nodes?
Default implementation for non-wildcard patterns.
"""
if len(nodes) != 1:
return False
return self.match(nodes[0], results)
def generate_matches(self, nodes: list[NL]) -> Iterator[tuple[int, _Results]]:
"""
Generator yielding all matches for this pattern.
Default implementation for non-wildcard patterns.
"""
r: _Results = {}
if nodes and self.match(nodes[0], r):
yield 1, r
class LeafPattern(BasePattern):
def __init__(
self,
type: int | None = None,
content: str | None = None,
name: str | None = None,
) -> None:
"""
Initializer. Takes optional type, content, and name.
The type, if given must be a token type (< 256). If not given,
this matches any *leaf* node; the content may still be required.
The content, if given, must be a string.
If a name is given, the matching node is stored in the results
dict under that key.
"""
if type is not None:
assert 0 <= type < 256, type
if content is not None:
assert isinstance(content, str), repr(content)
self.type = type
self.content = content
self.name = name
def match(self, node: NL, results=None) -> bool:
"""Override match() to insist on a leaf node."""
if not isinstance(node, Leaf):
return False
return BasePattern.match(self, node, results)
def _submatch(self, node, results=None):
"""
Match the pattern's content to the node's children.
This assumes the node type matches and self.content is not None.
Returns True if it matches, False if not.
If results is not None, it must be a dict which will be
updated with the nodes matching named subpatterns.
When returning False, the results dict may still be updated.
"""
return self.content == node.value
class NodePattern(BasePattern):
wildcards: bool = False
def __init__(
self,
type: int | None = None,
content: Iterable[str] | None = None,
name: str | None = None,
) -> None:
"""
Initializer. Takes optional type, content, and name.
The type, if given, must be a symbol type (>= 256). If the
type is None this matches *any* single node (leaf or not),
except if content is not None, in which it only matches
non-leaf nodes that also match the content pattern.
The content, if not None, must be a sequence of Patterns that
must match the node's children exactly. If the content is
given, the type must not be None.
If a name is given, the matching node is stored in the results
dict under that key.
"""
if type is not None:
assert type >= 256, type
if content is not None:
assert not isinstance(content, str), repr(content)
newcontent = list(content)
for i, item in enumerate(newcontent):
assert isinstance(item, BasePattern), (i, item)
# I don't even think this code is used anywhere, but it does cause
# unreachable errors from mypy. This function's signature does look
# odd though *shrug*.
if isinstance(item, WildcardPattern): # type: ignore[unreachable]
self.wildcards = True # type: ignore[unreachable]
self.type = type
self.content = newcontent # TODO: this is unbound when content is None
self.name = name
def _submatch(self, node, results=None) -> bool:
"""
Match the pattern's content to the node's children.
This assumes the node type matches and self.content is not None.
Returns True if it matches, False if not.
If results is not None, it must be a dict which will be
updated with the nodes matching named subpatterns.
When returning False, the results dict may still be updated.
"""
if self.wildcards:
for c, r in generate_matches(self.content, node.children):
if c == len(node.children):
if results is not None:
results.update(r)
return True
return False
if len(self.content) != len(node.children):
return False
for subpattern, child in zip(self.content, node.children):
if not subpattern.match(child, results):
return False
return True
class WildcardPattern(BasePattern):
"""
A wildcard pattern can match zero or more nodes.
This has all the flexibility needed to implement patterns like:
.* .+ .? .{m,n}
(a b c | d e | f)
(...)* (...)+ (...)? (...){m,n}
except it always uses non-greedy matching.
"""
min: int
max: int
def __init__(
self,
content: str | None = None,
min: int = 0,
max: int = HUGE,
name: str | None = None,
) -> None:
"""
Initializer.
Args:
content: optional sequence of subsequences of patterns;
if absent, matches one node;
if present, each subsequence is an alternative [*]
min: optional minimum number of times to match, default 0
max: optional maximum number of times to match, default HUGE
name: optional name assigned to this match
[*] Thus, if content is [[a, b, c], [d, e], [f, g, h]] this is
equivalent to (a b c | d e | f g h); if content is None,
this is equivalent to '.' in regular expression terms.
The min and max parameters work as follows:
min=0, max=maxint: .*
min=1, max=maxint: .+
min=0, max=1: .?
min=1, max=1: .
If content is not None, replace the dot with the parenthesized
list of alternatives, e.g. (a b c | d e | f g h)*
"""
assert 0 <= min <= max <= HUGE, (min, max)
if content is not None:
f = lambda s: tuple(s)
wrapped_content = tuple(map(f, content)) # Protect against alterations
# Check sanity of alternatives
assert len(wrapped_content), repr(
wrapped_content
) # Can't have zero alternatives
for alt in wrapped_content:
assert len(alt), repr(alt) # Can have empty alternatives
self.content = wrapped_content
self.min = min
self.max = max
self.name = name
def optimize(self) -> Any:
"""Optimize certain stacked wildcard patterns."""
subpattern = None
if (
self.content is not None
and len(self.content) == 1
and len(self.content[0]) == 1
):
subpattern = self.content[0][0]
if self.min == 1 and self.max == 1:
if self.content is None:
return NodePattern(name=self.name)
if subpattern is not None and self.name == subpattern.name:
return subpattern.optimize()
if (
self.min <= 1
and isinstance(subpattern, WildcardPattern)
and subpattern.min <= 1
and self.name == subpattern.name
):
return WildcardPattern(
subpattern.content,
self.min * subpattern.min,
self.max * subpattern.max,
subpattern.name,
)
return self
def match(self, node, results=None) -> bool:
"""Does this pattern exactly match a node?"""
return self.match_seq([node], results)
def match_seq(self, nodes, results=None) -> bool:
"""Does this pattern exactly match a sequence of nodes?"""
for c, r in self.generate_matches(nodes):
if c == len(nodes):
if results is not None:
results.update(r)
if self.name:
results[self.name] = list(nodes)
return True
return False
def generate_matches(self, nodes) -> Iterator[tuple[int, _Results]]:
"""
Generator yielding matches for a sequence of nodes.
Args:
nodes: sequence of nodes
Yields:
(count, results) tuples where:
count: the match comprises nodes[:count];
results: dict containing named submatches.
"""
if self.content is None:
# Shortcut for special case (see __init__.__doc__)
for count in range(self.min, 1 + min(len(nodes), self.max)):
r = {}
if self.name:
r[self.name] = nodes[:count]
yield count, r
elif self.name == "bare_name":
yield self._bare_name_matches(nodes)
else:
# The reason for this is that hitting the recursion limit usually
# results in some ugly messages about how RuntimeErrors are being
# ignored. We only have to do this on CPython, though, because other
# implementations don't have this nasty bug in the first place.
if hasattr(sys, "getrefcount"):
save_stderr = sys.stderr
sys.stderr = StringIO()
try:
for count, r in self._recursive_matches(nodes, 0):
if self.name:
r[self.name] = nodes[:count]
yield count, r
except RuntimeError:
# We fall back to the iterative pattern matching scheme if the recursive
# scheme hits the recursion limit.
for count, r in self._iterative_matches(nodes):
if self.name:
r[self.name] = nodes[:count]
yield count, r
finally:
if hasattr(sys, "getrefcount"):
sys.stderr = save_stderr
def _iterative_matches(self, nodes) -> Iterator[tuple[int, _Results]]:
"""Helper to iteratively yield the matches."""
nodelen = len(nodes)
if 0 >= self.min:
yield 0, {}
results = []
# generate matches that use just one alt from self.content
for alt in self.content:
for c, r in generate_matches(alt, nodes):
yield c, r
results.append((c, r))
# for each match, iterate down the nodes
while results:
new_results = []
for c0, r0 in results:
# stop if the entire set of nodes has been matched
if c0 < nodelen and c0 <= self.max:
for alt in self.content:
for c1, r1 in generate_matches(alt, nodes[c0:]):
if c1 > 0:
r = {}
r.update(r0)
r.update(r1)
yield c0 + c1, r
new_results.append((c0 + c1, r))
results = new_results
def _bare_name_matches(self, nodes) -> tuple[int, _Results]:
"""Special optimized matcher for bare_name."""
count = 0
r = {} # type: _Results
done = False
max = len(nodes)
while not done and count < max:
done = True
for leaf in self.content:
if leaf[0].match(nodes[count], r):
count += 1
done = False
break
assert self.name is not None
r[self.name] = nodes[:count]
return count, r
def _recursive_matches(self, nodes, count) -> Iterator[tuple[int, _Results]]:
"""Helper to recursively yield the matches."""
assert self.content is not None
if count >= self.min:
yield 0, {}
if count < self.max:
for alt in self.content:
for c0, r0 in generate_matches(alt, nodes):
for c1, r1 in self._recursive_matches(nodes[c0:], count + 1):
r = {}
r.update(r0)
r.update(r1)
yield c0 + c1, r
class NegatedPattern(BasePattern):
def __init__(self, content: BasePattern | None = None) -> None:
"""
Initializer.
The argument is either a pattern or None. If it is None, this
only matches an empty sequence (effectively '$' in regex
lingo). If it is not None, this matches whenever the argument
pattern doesn't have any matches.
"""
if content is not None:
assert isinstance(content, BasePattern), repr(content)
self.content = content
def match(self, node, results=None) -> bool:
# We never match a node in its entirety
return False
def match_seq(self, nodes, results=None) -> bool:
# We only match an empty sequence of nodes in its entirety
return len(nodes) == 0
def generate_matches(self, nodes: list[NL]) -> Iterator[tuple[int, _Results]]:
if self.content is None:
# Return a match if there is an empty sequence
if len(nodes) == 0:
yield 0, {}
else:
# Return a match if the argument pattern has no matches
for c, r in self.content.generate_matches(nodes):
return
yield 0, {}
def generate_matches(
patterns: list[BasePattern], nodes: list[NL]
) -> Iterator[tuple[int, _Results]]:
"""
Generator yielding matches for a sequence of patterns and nodes.
Args:
patterns: a sequence of patterns
nodes: a sequence of nodes
Yields:
(count, results) tuples where:
count: the entire sequence of patterns matches nodes[:count];
results: dict containing named submatches.
"""
if not patterns:
yield 0, {}
else:
p, rest = patterns[0], patterns[1:]
for c0, r0 in p.generate_matches(nodes):
if not rest:
yield c0, r0
else:
for c1, r1 in generate_matches(rest, nodes[c0:]):
r = {}
r.update(r0)
r.update(r1)
yield c0 + c1, r

View file

@ -0,0 +1 @@
pip

View file

@ -0,0 +1,84 @@
Metadata-Version: 2.4
Name: click
Version: 8.3.2
Summary: Composable command line interface toolkit
Maintainer-email: Pallets <contact@palletsprojects.com>
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-Expression: BSD-3-Clause
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Typing :: Typed
License-File: LICENSE.txt
Requires-Dist: colorama; platform_system == 'Windows'
Project-URL: Changes, https://click.palletsprojects.com/page/changes/
Project-URL: Chat, https://discord.gg/pallets
Project-URL: Documentation, https://click.palletsprojects.com/
Project-URL: Donate, https://palletsprojects.com/donate
Project-URL: Source, https://github.com/pallets/click/
<div align="center"><img src="https://raw.githubusercontent.com/pallets/click/refs/heads/stable/docs/_static/click-name.svg" alt="" height="150"></div>
# Click
Click is a Python package for creating beautiful command line interfaces
in a composable way with as little code as necessary. It's the "Command
Line Interface Creation Kit". It's highly configurable but comes with
sensible defaults out of the box.
It aims to make the process of writing command line tools quick and fun
while also preventing any frustration caused by the inability to
implement an intended CLI API.
Click in three points:
- Arbitrary nesting of commands
- Automatic help page generation
- Supports lazy loading of subcommands at runtime
## A Simple Example
```python
import click
@click.command()
@click.option("--count", default=1, help="Number of greetings.")
@click.option("--name", prompt="Your name", help="The person to greet.")
def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
for _ in range(count):
click.echo(f"Hello, {name}!")
if __name__ == '__main__':
hello()
```
```
$ python hello.py --count=3
Your name: Click
Hello, Click!
Hello, Click!
Hello, Click!
```
## Donate
The Pallets organization develops and supports Click and other popular
packages. In order to grow the community of contributors and users, and
allow the maintainers to devote more time to the projects, [please
donate today][].
[please donate today]: https://palletsprojects.com/donate
## Contributing
See our [detailed contributing documentation][contrib] for many ways to
contribute, including reporting issues, requesting features, asking or answering
questions, and making PRs.
[contrib]: https://palletsprojects.com/contributing/

View file

@ -0,0 +1,40 @@
click-8.3.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
click-8.3.2.dist-info/METADATA,sha256=yA3Hu5bMxtntTd4QrI8hTFAc58rHSPhDbP6bklbUQkA,2621
click-8.3.2.dist-info/RECORD,,
click-8.3.2.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
click-8.3.2.dist-info/licenses/LICENSE.txt,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475
click/__init__.py,sha256=6YyS1aeyknZ0LYweWozNZy0A9nZ_11wmYIhv3cbQrYo,4473
click/__pycache__/__init__.cpython-310.pyc,,
click/__pycache__/_compat.cpython-310.pyc,,
click/__pycache__/_termui_impl.cpython-310.pyc,,
click/__pycache__/_textwrap.cpython-310.pyc,,
click/__pycache__/_utils.cpython-310.pyc,,
click/__pycache__/_winconsole.cpython-310.pyc,,
click/__pycache__/core.cpython-310.pyc,,
click/__pycache__/decorators.cpython-310.pyc,,
click/__pycache__/exceptions.cpython-310.pyc,,
click/__pycache__/formatting.cpython-310.pyc,,
click/__pycache__/globals.cpython-310.pyc,,
click/__pycache__/parser.cpython-310.pyc,,
click/__pycache__/shell_completion.cpython-310.pyc,,
click/__pycache__/termui.cpython-310.pyc,,
click/__pycache__/testing.cpython-310.pyc,,
click/__pycache__/types.cpython-310.pyc,,
click/__pycache__/utils.cpython-310.pyc,,
click/_compat.py,sha256=v3xBZkFbvA1BXPRkFfBJc6-pIwPI7345m-kQEnpVAs4,18693
click/_termui_impl.py,sha256=rgCb3On8X5A4200rA5L6i13u5iapmFer7sru57Jy6zA,27093
click/_textwrap.py,sha256=BOae0RQ6vg3FkNgSJyOoGzG1meGMxJ_ukWVZKx_v-0o,1400
click/_utils.py,sha256=kZwtTf5gMuCilJJceS2iTCvRvCY-0aN5rJq8gKw7p8g,943
click/_winconsole.py,sha256=_vxUuUaxwBhoR0vUWCNuHY8VUefiMdCIyU2SXPqoF-A,8465
click/core.py,sha256=7db9qr_wqXbQriDHCDc26OK0MsaLCSt4yrz14Kn7AEQ,132905
click/decorators.py,sha256=5P7abhJtAQYp_KHgjUvhMv464ERwOzrv2enNknlwHyQ,18461
click/exceptions.py,sha256=8utf8w6V5hJXMnO_ic1FNrtbwuEn1NUu1aDwV8UqnG4,9954
click/formatting.py,sha256=RVfwwr0rwWNpgGr8NaHodPzkIr7_tUyVh_nDdanLMNc,9730
click/globals.py,sha256=gM-Nh6A4M0HB_SgkaF5M4ncGGMDHc_flHXu9_oh4GEU,1923
click/parser.py,sha256=Q31pH0FlQZEq-UXE_ABRzlygEfvxPTuZbWNh4xfXmzw,19010
click/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
click/shell_completion.py,sha256=Cc4GQUFuWpfQBa9sF5qXeeYI7n3tI_1k6ZdSn4BZbT0,20994
click/termui.py,sha256=hqCEjNndU-nzW08nRAkBaVgfZp_FdCA9KxfIWlKYaMc,31037
click/testing.py,sha256=LjNfHqNctxc3GfRkLgifO6gnRetblh8yGXzjw4FPFCQ,18978
click/types.py,sha256=ek54BNSFwPKsqtfT7jsqcc4WHui8AIFVMKM4oVZIXhc,39927
click/utils.py,sha256=gCUoewdAhA-QLBUUHxrLh4uj6m7T1WjZZMNPvR0I7YA,20257

View file

@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: flit 3.12.0
Root-Is-Purelib: true
Tag: py3-none-any

View file

@ -0,0 +1,28 @@
Copyright 2014 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,123 @@
"""
Click is a simple Python module inspired by the stdlib optparse to make
writing command line scripts fun. Unlike other modules, it's based
around a simple API that does not come with too much magic and is
composable.
"""
from __future__ import annotations
from .core import Argument as Argument
from .core import Command as Command
from .core import CommandCollection as CommandCollection
from .core import Context as Context
from .core import Group as Group
from .core import Option as Option
from .core import Parameter as Parameter
from .decorators import argument as argument
from .decorators import command as command
from .decorators import confirmation_option as confirmation_option
from .decorators import group as group
from .decorators import help_option as help_option
from .decorators import make_pass_decorator as make_pass_decorator
from .decorators import option as option
from .decorators import pass_context as pass_context
from .decorators import pass_obj as pass_obj
from .decorators import password_option as password_option
from .decorators import version_option as version_option
from .exceptions import Abort as Abort
from .exceptions import BadArgumentUsage as BadArgumentUsage
from .exceptions import BadOptionUsage as BadOptionUsage
from .exceptions import BadParameter as BadParameter
from .exceptions import ClickException as ClickException
from .exceptions import FileError as FileError
from .exceptions import MissingParameter as MissingParameter
from .exceptions import NoSuchOption as NoSuchOption
from .exceptions import UsageError as UsageError
from .formatting import HelpFormatter as HelpFormatter
from .formatting import wrap_text as wrap_text
from .globals import get_current_context as get_current_context
from .termui import clear as clear
from .termui import confirm as confirm
from .termui import echo_via_pager as echo_via_pager
from .termui import edit as edit
from .termui import getchar as getchar
from .termui import launch as launch
from .termui import pause as pause
from .termui import progressbar as progressbar
from .termui import prompt as prompt
from .termui import secho as secho
from .termui import style as style
from .termui import unstyle as unstyle
from .types import BOOL as BOOL
from .types import Choice as Choice
from .types import DateTime as DateTime
from .types import File as File
from .types import FLOAT as FLOAT
from .types import FloatRange as FloatRange
from .types import INT as INT
from .types import IntRange as IntRange
from .types import ParamType as ParamType
from .types import Path as Path
from .types import STRING as STRING
from .types import Tuple as Tuple
from .types import UNPROCESSED as UNPROCESSED
from .types import UUID as UUID
from .utils import echo as echo
from .utils import format_filename as format_filename
from .utils import get_app_dir as get_app_dir
from .utils import get_binary_stream as get_binary_stream
from .utils import get_text_stream as get_text_stream
from .utils import open_file as open_file
def __getattr__(name: str) -> object:
import warnings
if name == "BaseCommand":
from .core import _BaseCommand
warnings.warn(
"'BaseCommand' is deprecated and will be removed in Click 9.0. Use"
" 'Command' instead.",
DeprecationWarning,
stacklevel=2,
)
return _BaseCommand
if name == "MultiCommand":
from .core import _MultiCommand
warnings.warn(
"'MultiCommand' is deprecated and will be removed in Click 9.0. Use"
" 'Group' instead.",
DeprecationWarning,
stacklevel=2,
)
return _MultiCommand
if name == "OptionParser":
from .parser import _OptionParser
warnings.warn(
"'OptionParser' is deprecated and will be removed in Click 9.0. The"
" old parser is available in 'optparse'.",
DeprecationWarning,
stacklevel=2,
)
return _OptionParser
if name == "__version__":
import importlib.metadata
import warnings
warnings.warn(
"The '__version__' attribute is deprecated and will be removed in"
" Click 9.1. Use feature detection or"
" 'importlib.metadata.version(\"click\")' instead.",
DeprecationWarning,
stacklevel=2,
)
return importlib.metadata.version("click")
raise AttributeError(name)

View file

@ -0,0 +1,622 @@
from __future__ import annotations
import codecs
import collections.abc as cabc
import io
import os
import re
import sys
import typing as t
from types import TracebackType
from weakref import WeakKeyDictionary
CYGWIN = sys.platform.startswith("cygwin")
WIN = sys.platform.startswith("win")
auto_wrap_for_ansi: t.Callable[[t.TextIO], t.TextIO] | None = None
_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]")
def _make_text_stream(
stream: t.BinaryIO,
encoding: str | None,
errors: str | None,
force_readable: bool = False,
force_writable: bool = False,
) -> t.TextIO:
if encoding is None:
encoding = get_best_encoding(stream)
if errors is None:
errors = "replace"
return _NonClosingTextIOWrapper(
stream,
encoding,
errors,
line_buffering=True,
force_readable=force_readable,
force_writable=force_writable,
)
def is_ascii_encoding(encoding: str) -> bool:
"""Checks if a given encoding is ascii."""
try:
return codecs.lookup(encoding).name == "ascii"
except LookupError:
return False
def get_best_encoding(stream: t.IO[t.Any]) -> str:
"""Returns the default stream encoding if not found."""
rv = getattr(stream, "encoding", None) or sys.getdefaultencoding()
if is_ascii_encoding(rv):
return "utf-8"
return rv
class _NonClosingTextIOWrapper(io.TextIOWrapper):
def __init__(
self,
stream: t.BinaryIO,
encoding: str | None,
errors: str | None,
force_readable: bool = False,
force_writable: bool = False,
**extra: t.Any,
) -> None:
self._stream = stream = t.cast(
t.BinaryIO, _FixupStream(stream, force_readable, force_writable)
)
super().__init__(stream, encoding, errors, **extra)
def __del__(self) -> None:
try:
self.detach()
except Exception:
pass
def isatty(self) -> bool:
# https://bitbucket.org/pypy/pypy/issue/1803
return self._stream.isatty()
class _FixupStream:
"""The new io interface needs more from streams than streams
traditionally implement. As such, this fix-up code is necessary in
some circumstances.
The forcing of readable and writable flags are there because some tools
put badly patched objects on sys (one such offender are certain version
of jupyter notebook).
"""
def __init__(
self,
stream: t.BinaryIO,
force_readable: bool = False,
force_writable: bool = False,
):
self._stream = stream
self._force_readable = force_readable
self._force_writable = force_writable
def __getattr__(self, name: str) -> t.Any:
return getattr(self._stream, name)
def read1(self, size: int) -> bytes:
f = getattr(self._stream, "read1", None)
if f is not None:
return t.cast(bytes, f(size))
return self._stream.read(size)
def readable(self) -> bool:
if self._force_readable:
return True
x = getattr(self._stream, "readable", None)
if x is not None:
return t.cast(bool, x())
try:
self._stream.read(0)
except Exception:
return False
return True
def writable(self) -> bool:
if self._force_writable:
return True
x = getattr(self._stream, "writable", None)
if x is not None:
return t.cast(bool, x())
try:
self._stream.write(b"")
except Exception:
try:
self._stream.write(b"")
except Exception:
return False
return True
def seekable(self) -> bool:
x = getattr(self._stream, "seekable", None)
if x is not None:
return t.cast(bool, x())
try:
self._stream.seek(self._stream.tell())
except Exception:
return False
return True
def _is_binary_reader(stream: t.IO[t.Any], default: bool = False) -> bool:
try:
return isinstance(stream.read(0), bytes)
except Exception:
return default
# This happens in some cases where the stream was already
# closed. In this case, we assume the default.
def _is_binary_writer(stream: t.IO[t.Any], default: bool = False) -> bool:
try:
stream.write(b"")
except Exception:
try:
stream.write("")
return False
except Exception:
pass
return default
return True
def _find_binary_reader(stream: t.IO[t.Any]) -> t.BinaryIO | None:
# We need to figure out if the given stream is already binary.
# This can happen because the official docs recommend detaching
# the streams to get binary streams. Some code might do this, so
# we need to deal with this case explicitly.
if _is_binary_reader(stream, False):
return t.cast(t.BinaryIO, stream)
buf = getattr(stream, "buffer", None)
# Same situation here; this time we assume that the buffer is
# actually binary in case it's closed.
if buf is not None and _is_binary_reader(buf, True):
return t.cast(t.BinaryIO, buf)
return None
def _find_binary_writer(stream: t.IO[t.Any]) -> t.BinaryIO | None:
# We need to figure out if the given stream is already binary.
# This can happen because the official docs recommend detaching
# the streams to get binary streams. Some code might do this, so
# we need to deal with this case explicitly.
if _is_binary_writer(stream, False):
return t.cast(t.BinaryIO, stream)
buf = getattr(stream, "buffer", None)
# Same situation here; this time we assume that the buffer is
# actually binary in case it's closed.
if buf is not None and _is_binary_writer(buf, True):
return t.cast(t.BinaryIO, buf)
return None
def _stream_is_misconfigured(stream: t.TextIO) -> bool:
"""A stream is misconfigured if its encoding is ASCII."""
# If the stream does not have an encoding set, we assume it's set
# to ASCII. This appears to happen in certain unittest
# environments. It's not quite clear what the correct behavior is
# but this at least will force Click to recover somehow.
return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii")
def _is_compat_stream_attr(stream: t.TextIO, attr: str, value: str | None) -> bool:
"""A stream attribute is compatible if it is equal to the
desired value or the desired value is unset and the attribute
has a value.
"""
stream_value = getattr(stream, attr, None)
return stream_value == value or (value is None and stream_value is not None)
def _is_compatible_text_stream(
stream: t.TextIO, encoding: str | None, errors: str | None
) -> bool:
"""Check if a stream's encoding and errors attributes are
compatible with the desired values.
"""
return _is_compat_stream_attr(
stream, "encoding", encoding
) and _is_compat_stream_attr(stream, "errors", errors)
def _force_correct_text_stream(
text_stream: t.IO[t.Any],
encoding: str | None,
errors: str | None,
is_binary: t.Callable[[t.IO[t.Any], bool], bool],
find_binary: t.Callable[[t.IO[t.Any]], t.BinaryIO | None],
force_readable: bool = False,
force_writable: bool = False,
) -> t.TextIO:
if is_binary(text_stream, False):
binary_reader = t.cast(t.BinaryIO, text_stream)
else:
text_stream = t.cast(t.TextIO, text_stream)
# If the stream looks compatible, and won't default to a
# misconfigured ascii encoding, return it as-is.
if _is_compatible_text_stream(text_stream, encoding, errors) and not (
encoding is None and _stream_is_misconfigured(text_stream)
):
return text_stream
# Otherwise, get the underlying binary reader.
possible_binary_reader = find_binary(text_stream)
# If that's not possible, silently use the original reader
# and get mojibake instead of exceptions.
if possible_binary_reader is None:
return text_stream
binary_reader = possible_binary_reader
# Default errors to replace instead of strict in order to get
# something that works.
if errors is None:
errors = "replace"
# Wrap the binary stream in a text stream with the correct
# encoding parameters.
return _make_text_stream(
binary_reader,
encoding,
errors,
force_readable=force_readable,
force_writable=force_writable,
)
def _force_correct_text_reader(
text_reader: t.IO[t.Any],
encoding: str | None,
errors: str | None,
force_readable: bool = False,
) -> t.TextIO:
return _force_correct_text_stream(
text_reader,
encoding,
errors,
_is_binary_reader,
_find_binary_reader,
force_readable=force_readable,
)
def _force_correct_text_writer(
text_writer: t.IO[t.Any],
encoding: str | None,
errors: str | None,
force_writable: bool = False,
) -> t.TextIO:
return _force_correct_text_stream(
text_writer,
encoding,
errors,
_is_binary_writer,
_find_binary_writer,
force_writable=force_writable,
)
def get_binary_stdin() -> t.BinaryIO:
reader = _find_binary_reader(sys.stdin)
if reader is None:
raise RuntimeError("Was not able to determine binary stream for sys.stdin.")
return reader
def get_binary_stdout() -> t.BinaryIO:
writer = _find_binary_writer(sys.stdout)
if writer is None:
raise RuntimeError("Was not able to determine binary stream for sys.stdout.")
return writer
def get_binary_stderr() -> t.BinaryIO:
writer = _find_binary_writer(sys.stderr)
if writer is None:
raise RuntimeError("Was not able to determine binary stream for sys.stderr.")
return writer
def get_text_stdin(encoding: str | None = None, errors: str | None = None) -> t.TextIO:
rv = _get_windows_console_stream(sys.stdin, encoding, errors)
if rv is not None:
return rv
return _force_correct_text_reader(sys.stdin, encoding, errors, force_readable=True)
def get_text_stdout(encoding: str | None = None, errors: str | None = None) -> t.TextIO:
rv = _get_windows_console_stream(sys.stdout, encoding, errors)
if rv is not None:
return rv
return _force_correct_text_writer(sys.stdout, encoding, errors, force_writable=True)
def get_text_stderr(encoding: str | None = None, errors: str | None = None) -> t.TextIO:
rv = _get_windows_console_stream(sys.stderr, encoding, errors)
if rv is not None:
return rv
return _force_correct_text_writer(sys.stderr, encoding, errors, force_writable=True)
def _wrap_io_open(
file: str | os.PathLike[str] | int,
mode: str,
encoding: str | None,
errors: str | None,
) -> t.IO[t.Any]:
"""Handles not passing ``encoding`` and ``errors`` in binary mode."""
if "b" in mode:
return open(file, mode)
return open(file, mode, encoding=encoding, errors=errors)
def open_stream(
filename: str | os.PathLike[str],
mode: str = "r",
encoding: str | None = None,
errors: str | None = "strict",
atomic: bool = False,
) -> tuple[t.IO[t.Any], bool]:
binary = "b" in mode
filename = os.fspath(filename)
# Standard streams first. These are simple because they ignore the
# atomic flag. Use fsdecode to handle Path("-").
if os.fsdecode(filename) == "-":
if any(m in mode for m in ["w", "a", "x"]):
if binary:
return get_binary_stdout(), False
return get_text_stdout(encoding=encoding, errors=errors), False
if binary:
return get_binary_stdin(), False
return get_text_stdin(encoding=encoding, errors=errors), False
# Non-atomic writes directly go out through the regular open functions.
if not atomic:
return _wrap_io_open(filename, mode, encoding, errors), True
# Some usability stuff for atomic writes
if "a" in mode:
raise ValueError(
"Appending to an existing file is not supported, because that"
" would involve an expensive `copy`-operation to a temporary"
" file. Open the file in normal `w`-mode and copy explicitly"
" if that's what you're after."
)
if "x" in mode:
raise ValueError("Use the `overwrite`-parameter instead.")
if "w" not in mode:
raise ValueError("Atomic writes only make sense with `w`-mode.")
# Atomic writes are more complicated. They work by opening a file
# as a proxy in the same folder and then using the fdopen
# functionality to wrap it in a Python file. Then we wrap it in an
# atomic file that moves the file over on close.
import errno
import random
try:
perm: int | None = os.stat(filename).st_mode
except OSError:
perm = None
flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
if binary:
flags |= getattr(os, "O_BINARY", 0)
while True:
tmp_filename = os.path.join(
os.path.dirname(filename),
f".__atomic-write{random.randrange(1 << 32):08x}",
)
try:
fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm)
break
except OSError as e:
if e.errno == errno.EEXIST or (
os.name == "nt"
and e.errno == errno.EACCES
and os.path.isdir(e.filename)
and os.access(e.filename, os.W_OK)
):
continue
raise
if perm is not None:
os.chmod(tmp_filename, perm) # in case perm includes bits in umask
f = _wrap_io_open(fd, mode, encoding, errors)
af = _AtomicFile(f, tmp_filename, os.path.realpath(filename))
return t.cast(t.IO[t.Any], af), True
class _AtomicFile:
def __init__(self, f: t.IO[t.Any], tmp_filename: str, real_filename: str) -> None:
self._f = f
self._tmp_filename = tmp_filename
self._real_filename = real_filename
self.closed = False
@property
def name(self) -> str:
return self._real_filename
def close(self, delete: bool = False) -> None:
if self.closed:
return
self._f.close()
os.replace(self._tmp_filename, self._real_filename)
self.closed = True
def __getattr__(self, name: str) -> t.Any:
return getattr(self._f, name)
def __enter__(self) -> _AtomicFile:
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
tb: TracebackType | None,
) -> None:
self.close(delete=exc_type is not None)
def __repr__(self) -> str:
return repr(self._f)
def strip_ansi(value: str) -> str:
return _ansi_re.sub("", value)
def _is_jupyter_kernel_output(stream: t.IO[t.Any]) -> bool:
while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):
stream = stream._stream
return stream.__class__.__module__.startswith("ipykernel.")
def should_strip_ansi(
stream: t.IO[t.Any] | None = None, color: bool | None = None
) -> bool:
if color is None:
if stream is None:
stream = sys.stdin
return not isatty(stream) and not _is_jupyter_kernel_output(stream)
return not color
# On Windows, wrap the output streams with colorama to support ANSI
# color codes.
# NOTE: double check is needed so mypy does not analyze this on Linux
if sys.platform.startswith("win") and WIN:
from ._winconsole import _get_windows_console_stream
def _get_argv_encoding() -> str:
import locale
return locale.getpreferredencoding()
_ansi_stream_wrappers: cabc.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary()
def auto_wrap_for_ansi(stream: t.TextIO, color: bool | None = None) -> t.TextIO:
"""Support ANSI color and style codes on Windows by wrapping a
stream with colorama.
"""
try:
cached = _ansi_stream_wrappers.get(stream)
except Exception:
cached = None
if cached is not None:
return cached
import colorama
strip = should_strip_ansi(stream, color)
ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)
rv = t.cast(t.TextIO, ansi_wrapper.stream)
_write = rv.write
def _safe_write(s: str) -> int:
try:
return _write(s)
except BaseException:
ansi_wrapper.reset_all()
raise
rv.write = _safe_write # type: ignore[method-assign]
try:
_ansi_stream_wrappers[stream] = rv
except Exception:
pass
return rv
else:
def _get_argv_encoding() -> str:
return getattr(sys.stdin, "encoding", None) or sys.getfilesystemencoding()
def _get_windows_console_stream(
f: t.TextIO, encoding: str | None, errors: str | None
) -> t.TextIO | None:
return None
def term_len(x: str) -> int:
return len(strip_ansi(x))
def isatty(stream: t.IO[t.Any]) -> bool:
try:
return stream.isatty()
except Exception:
return False
def _make_cached_stream_func(
src_func: t.Callable[[], t.TextIO | None],
wrapper_func: t.Callable[[], t.TextIO],
) -> t.Callable[[], t.TextIO | None]:
cache: cabc.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary()
def func() -> t.TextIO | None:
stream = src_func()
if stream is None:
return None
try:
rv = cache.get(stream)
except Exception:
rv = None
if rv is not None:
return rv
rv = wrapper_func()
try:
cache[stream] = rv
except Exception:
pass
return rv
return func
_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin)
_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout)
_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr)
binary_streams: cabc.Mapping[str, t.Callable[[], t.BinaryIO]] = {
"stdin": get_binary_stdin,
"stdout": get_binary_stdout,
"stderr": get_binary_stderr,
}
text_streams: cabc.Mapping[str, t.Callable[[str | None, str | None], t.TextIO]] = {
"stdin": get_text_stdin,
"stdout": get_text_stdout,
"stderr": get_text_stderr,
}

View file

@ -0,0 +1,852 @@
"""
This module contains implementations for the termui module. To keep the
import time of Click down, some infrequently used functionality is
placed in this module and only imported as needed.
"""
from __future__ import annotations
import collections.abc as cabc
import contextlib
import math
import os
import shlex
import sys
import time
import typing as t
from gettext import gettext as _
from io import StringIO
from pathlib import Path
from types import TracebackType
from ._compat import _default_text_stdout
from ._compat import CYGWIN
from ._compat import get_best_encoding
from ._compat import isatty
from ._compat import open_stream
from ._compat import strip_ansi
from ._compat import term_len
from ._compat import WIN
from .exceptions import ClickException
from .utils import echo
V = t.TypeVar("V")
if os.name == "nt":
BEFORE_BAR = "\r"
AFTER_BAR = "\n"
else:
BEFORE_BAR = "\r\033[?25l"
AFTER_BAR = "\033[?25h\n"
class ProgressBar(t.Generic[V]):
def __init__(
self,
iterable: cabc.Iterable[V] | None,
length: int | None = None,
fill_char: str = "#",
empty_char: str = " ",
bar_template: str = "%(bar)s",
info_sep: str = " ",
hidden: bool = False,
show_eta: bool = True,
show_percent: bool | None = None,
show_pos: bool = False,
item_show_func: t.Callable[[V | None], str | None] | None = None,
label: str | None = None,
file: t.TextIO | None = None,
color: bool | None = None,
update_min_steps: int = 1,
width: int = 30,
) -> None:
self.fill_char = fill_char
self.empty_char = empty_char
self.bar_template = bar_template
self.info_sep = info_sep
self.hidden = hidden
self.show_eta = show_eta
self.show_percent = show_percent
self.show_pos = show_pos
self.item_show_func = item_show_func
self.label: str = label or ""
if file is None:
file = _default_text_stdout()
# There are no standard streams attached to write to. For example,
# pythonw on Windows.
if file is None:
file = StringIO()
self.file = file
self.color = color
self.update_min_steps = update_min_steps
self._completed_intervals = 0
self.width: int = width
self.autowidth: bool = width == 0
if length is None:
from operator import length_hint
length = length_hint(iterable, -1)
if length == -1:
length = None
if iterable is None:
if length is None:
raise TypeError("iterable or length is required")
iterable = t.cast("cabc.Iterable[V]", range(length))
self.iter: cabc.Iterable[V] = iter(iterable)
self.length = length
self.pos: int = 0
self.avg: list[float] = []
self.last_eta: float
self.start: float
self.start = self.last_eta = time.time()
self.eta_known: bool = False
self.finished: bool = False
self.max_width: int | None = None
self.entered: bool = False
self.current_item: V | None = None
self._is_atty = isatty(self.file)
self._last_line: str | None = None
def __enter__(self) -> ProgressBar[V]:
self.entered = True
self.render_progress()
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
tb: TracebackType | None,
) -> None:
self.render_finish()
def __iter__(self) -> cabc.Iterator[V]:
if not self.entered:
raise RuntimeError("You need to use progress bars in a with block.")
self.render_progress()
return self.generator()
def __next__(self) -> V:
# Iteration is defined in terms of a generator function,
# returned by iter(self); use that to define next(). This works
# because `self.iter` is an iterable consumed by that generator,
# so it is re-entry safe. Calling `next(self.generator())`
# twice works and does "what you want".
return next(iter(self))
def render_finish(self) -> None:
if self.hidden or not self._is_atty:
return
self.file.write(AFTER_BAR)
self.file.flush()
@property
def pct(self) -> float:
if self.finished:
return 1.0
return min(self.pos / (float(self.length or 1) or 1), 1.0)
@property
def time_per_iteration(self) -> float:
if not self.avg:
return 0.0
return sum(self.avg) / float(len(self.avg))
@property
def eta(self) -> float:
if self.length is not None and not self.finished:
return self.time_per_iteration * (self.length - self.pos)
return 0.0
def format_eta(self) -> str:
if self.eta_known:
t = int(self.eta)
seconds = t % 60
t //= 60
minutes = t % 60
t //= 60
hours = t % 24
t //= 24
if t > 0:
return f"{t}d {hours:02}:{minutes:02}:{seconds:02}"
else:
return f"{hours:02}:{minutes:02}:{seconds:02}"
return ""
def format_pos(self) -> str:
pos = str(self.pos)
if self.length is not None:
pos += f"/{self.length}"
return pos
def format_pct(self) -> str:
return f"{int(self.pct * 100): 4}%"[1:]
def format_bar(self) -> str:
if self.length is not None:
bar_length = int(self.pct * self.width)
bar = self.fill_char * bar_length
bar += self.empty_char * (self.width - bar_length)
elif self.finished:
bar = self.fill_char * self.width
else:
chars = list(self.empty_char * (self.width or 1))
if self.time_per_iteration != 0:
chars[
int(
(math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5)
* self.width
)
] = self.fill_char
bar = "".join(chars)
return bar
def format_progress_line(self) -> str:
show_percent = self.show_percent
info_bits = []
if self.length is not None and show_percent is None:
show_percent = not self.show_pos
if self.show_pos:
info_bits.append(self.format_pos())
if show_percent:
info_bits.append(self.format_pct())
if self.show_eta and self.eta_known and not self.finished:
info_bits.append(self.format_eta())
if self.item_show_func is not None:
item_info = self.item_show_func(self.current_item)
if item_info is not None:
info_bits.append(item_info)
return (
self.bar_template
% {
"label": self.label,
"bar": self.format_bar(),
"info": self.info_sep.join(info_bits),
}
).rstrip()
def render_progress(self) -> None:
if self.hidden:
return
if not self._is_atty:
# Only output the label once if the output is not a TTY.
if self._last_line != self.label:
self._last_line = self.label
echo(self.label, file=self.file, color=self.color)
return
buf = []
# Update width in case the terminal has been resized
if self.autowidth:
import shutil
old_width = self.width
self.width = 0
clutter_length = term_len(self.format_progress_line())
new_width = max(0, shutil.get_terminal_size().columns - clutter_length)
if new_width < old_width and self.max_width is not None:
buf.append(BEFORE_BAR)
buf.append(" " * self.max_width)
self.max_width = new_width
self.width = new_width
clear_width = self.width
if self.max_width is not None:
clear_width = self.max_width
buf.append(BEFORE_BAR)
line = self.format_progress_line()
line_len = term_len(line)
if self.max_width is None or self.max_width < line_len:
self.max_width = line_len
buf.append(line)
buf.append(" " * (clear_width - line_len))
line = "".join(buf)
# Render the line only if it changed.
if line != self._last_line:
self._last_line = line
echo(line, file=self.file, color=self.color, nl=False)
self.file.flush()
def make_step(self, n_steps: int) -> None:
self.pos += n_steps
if self.length is not None and self.pos >= self.length:
self.finished = True
if (time.time() - self.last_eta) < 1.0:
return
self.last_eta = time.time()
# self.avg is a rolling list of length <= 7 of steps where steps are
# defined as time elapsed divided by the total progress through
# self.length.
if self.pos:
step = (time.time() - self.start) / self.pos
else:
step = time.time() - self.start
self.avg = self.avg[-6:] + [step]
self.eta_known = self.length is not None
def update(self, n_steps: int, current_item: V | None = None) -> None:
"""Update the progress bar by advancing a specified number of
steps, and optionally set the ``current_item`` for this new
position.
:param n_steps: Number of steps to advance.
:param current_item: Optional item to set as ``current_item``
for the updated position.
.. versionchanged:: 8.0
Added the ``current_item`` optional parameter.
.. versionchanged:: 8.0
Only render when the number of steps meets the
``update_min_steps`` threshold.
"""
if current_item is not None:
self.current_item = current_item
self._completed_intervals += n_steps
if self._completed_intervals >= self.update_min_steps:
self.make_step(self._completed_intervals)
self.render_progress()
self._completed_intervals = 0
def finish(self) -> None:
self.eta_known = False
self.current_item = None
self.finished = True
def generator(self) -> cabc.Iterator[V]:
"""Return a generator which yields the items added to the bar
during construction, and updates the progress bar *after* the
yielded block returns.
"""
# WARNING: the iterator interface for `ProgressBar` relies on
# this and only works because this is a simple generator which
# doesn't create or manage additional state. If this function
# changes, the impact should be evaluated both against
# `iter(bar)` and `next(bar)`. `next()` in particular may call
# `self.generator()` repeatedly, and this must remain safe in
# order for that interface to work.
if not self.entered:
raise RuntimeError("You need to use progress bars in a with block.")
if not self._is_atty:
yield from self.iter
else:
for rv in self.iter:
self.current_item = rv
# This allows show_item_func to be updated before the
# item is processed. Only trigger at the beginning of
# the update interval.
if self._completed_intervals == 0:
self.render_progress()
yield rv
self.update(1)
self.finish()
self.render_progress()
def pager(generator: cabc.Iterable[str], color: bool | None = None) -> None:
"""Decide what method to use for paging through text."""
stdout = _default_text_stdout()
# There are no standard streams attached to write to. For example,
# pythonw on Windows.
if stdout is None:
stdout = StringIO()
if not isatty(sys.stdin) or not isatty(stdout):
return _nullpager(stdout, generator, color)
# Split and normalize the pager command into parts.
pager_cmd_parts = shlex.split(os.environ.get("PAGER", ""), posix=False)
if pager_cmd_parts:
if WIN:
if _tempfilepager(generator, pager_cmd_parts, color):
return
elif _pipepager(generator, pager_cmd_parts, color):
return
if os.environ.get("TERM") in ("dumb", "emacs"):
return _nullpager(stdout, generator, color)
if (WIN or sys.platform.startswith("os2")) and _tempfilepager(
generator, ["more"], color
):
return
if _pipepager(generator, ["less"], color):
return
import tempfile
fd, filename = tempfile.mkstemp()
os.close(fd)
try:
if _pipepager(generator, ["more"], color):
return
return _nullpager(stdout, generator, color)
finally:
os.unlink(filename)
def _pipepager(
generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None
) -> bool:
"""Page through text by feeding it to another program. Invoking a
pager through this might support colors.
Returns `True` if the command was found, `False` otherwise and thus another
pager should be attempted.
"""
# Split the command into the invoked CLI and its parameters.
if not cmd_parts:
return False
import shutil
cmd = cmd_parts[0]
cmd_params = cmd_parts[1:]
cmd_filepath = shutil.which(cmd)
if not cmd_filepath:
return False
# Produces a normalized absolute path string.
# multi-call binaries such as busybox derive their identity from the symlink
# less -> busybox. resolve() causes them to misbehave. (eg. less becomes busybox)
cmd_path = Path(cmd_filepath).absolute()
cmd_name = cmd_path.name
import subprocess
# Make a local copy of the environment to not affect the global one.
env = dict(os.environ)
# If we're piping to less and the user hasn't decided on colors, we enable
# them by default we find the -R flag in the command line arguments.
if color is None and cmd_name == "less":
less_flags = f"{os.environ.get('LESS', '')}{' '.join(cmd_params)}"
if not less_flags:
env["LESS"] = "-R"
color = True
elif "r" in less_flags or "R" in less_flags:
color = True
c = subprocess.Popen(
[str(cmd_path)] + cmd_params,
shell=False,
stdin=subprocess.PIPE,
env=env,
errors="replace",
text=True,
)
assert c.stdin is not None
try:
for text in generator:
if not color:
text = strip_ansi(text)
c.stdin.write(text)
except BrokenPipeError:
# In case the pager exited unexpectedly, ignore the broken pipe error.
pass
except Exception as e:
# In case there is an exception we want to close the pager immediately
# and let the caller handle it.
# Otherwise the pager will keep running, and the user may not notice
# the error message, or worse yet it may leave the terminal in a broken state.
c.terminate()
raise e
finally:
# We must close stdin and wait for the pager to exit before we continue
try:
c.stdin.close()
# Close implies flush, so it might throw a BrokenPipeError if the pager
# process exited already.
except BrokenPipeError:
pass
# Less doesn't respect ^C, but catches it for its own UI purposes (aborting
# search or other commands inside less).
#
# That means when the user hits ^C, the parent process (click) terminates,
# but less is still alive, paging the output and messing up the terminal.
#
# If the user wants to make the pager exit on ^C, they should set
# `LESS='-K'`. It's not our decision to make.
while True:
try:
c.wait()
except KeyboardInterrupt:
pass
else:
break
return True
def _tempfilepager(
generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None
) -> bool:
"""Page through text by invoking a program on a temporary file.
Returns `True` if the command was found, `False` otherwise and thus another
pager should be attempted.
"""
# Split the command into the invoked CLI and its parameters.
if not cmd_parts:
return False
import shutil
cmd = cmd_parts[0]
cmd_filepath = shutil.which(cmd)
if not cmd_filepath:
return False
# Produces a normalized absolute path string.
# multi-call binaries such as busybox derive their identity from the symlink
# less -> busybox. resolve() causes them to misbehave. (eg. less becomes busybox)
cmd_path = Path(cmd_filepath).absolute()
import subprocess
import tempfile
fd, filename = tempfile.mkstemp()
# TODO: This never terminates if the passed generator never terminates.
text = "".join(generator)
if not color:
text = strip_ansi(text)
encoding = get_best_encoding(sys.stdout)
with open_stream(filename, "wb")[0] as f:
f.write(text.encode(encoding))
try:
subprocess.call([str(cmd_path), filename])
except OSError:
# Command not found
pass
finally:
os.close(fd)
os.unlink(filename)
return True
def _nullpager(
stream: t.TextIO, generator: cabc.Iterable[str], color: bool | None
) -> None:
"""Simply print unformatted text. This is the ultimate fallback."""
for text in generator:
if not color:
text = strip_ansi(text)
stream.write(text)
class Editor:
def __init__(
self,
editor: str | None = None,
env: cabc.Mapping[str, str] | None = None,
require_save: bool = True,
extension: str = ".txt",
) -> None:
self.editor = editor
self.env = env
self.require_save = require_save
self.extension = extension
def get_editor(self) -> str:
if self.editor is not None:
return self.editor
for key in "VISUAL", "EDITOR":
rv = os.environ.get(key)
if rv:
return rv
if WIN:
return "notepad"
from shutil import which
for editor in "sensible-editor", "vim", "nano":
if which(editor) is not None:
return editor
return "vi"
def edit_files(self, filenames: cabc.Iterable[str]) -> None:
import subprocess
editor = self.get_editor()
environ: dict[str, str] | None = None
if self.env:
environ = os.environ.copy()
environ.update(self.env)
exc_filename = " ".join(f'"{filename}"' for filename in filenames)
try:
c = subprocess.Popen(
args=f"{editor} {exc_filename}", env=environ, shell=True
)
exit_code = c.wait()
if exit_code != 0:
raise ClickException(
_("{editor}: Editing failed").format(editor=editor)
)
except OSError as e:
raise ClickException(
_("{editor}: Editing failed: {e}").format(editor=editor, e=e)
) from e
@t.overload
def edit(self, text: bytes | bytearray) -> bytes | None: ...
# We cannot know whether or not the type expected is str or bytes when None
# is passed, so str is returned as that was what was done before.
@t.overload
def edit(self, text: str | None) -> str | None: ...
def edit(self, text: str | bytes | bytearray | None) -> str | bytes | None:
import tempfile
if text is None:
data: bytes | bytearray = b""
elif isinstance(text, (bytes, bytearray)):
data = text
else:
if text and not text.endswith("\n"):
text += "\n"
if WIN:
data = text.replace("\n", "\r\n").encode("utf-8-sig")
else:
data = text.encode("utf-8")
fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension)
f: t.BinaryIO
try:
with os.fdopen(fd, "wb") as f:
f.write(data)
# If the filesystem resolution is 1 second, like Mac OS
# 10.12 Extended, or 2 seconds, like FAT32, and the editor
# closes very fast, require_save can fail. Set the modified
# time to be 2 seconds in the past to work around this.
os.utime(name, (os.path.getatime(name), os.path.getmtime(name) - 2))
# Depending on the resolution, the exact value might not be
# recorded, so get the new recorded value.
timestamp = os.path.getmtime(name)
self.edit_files((name,))
if self.require_save and os.path.getmtime(name) == timestamp:
return None
with open(name, "rb") as f:
rv = f.read()
if isinstance(text, (bytes, bytearray)):
return rv
return rv.decode("utf-8-sig").replace("\r\n", "\n")
finally:
os.unlink(name)
def open_url(url: str, wait: bool = False, locate: bool = False) -> int:
import subprocess
def _unquote_file(url: str) -> str:
from urllib.parse import unquote
if url.startswith("file://"):
url = unquote(url[7:])
return url
if sys.platform == "darwin":
args = ["open"]
if wait:
args.append("-W")
if locate:
args.append("-R")
args.append(_unquote_file(url))
null = open("/dev/null", "w")
try:
return subprocess.Popen(args, stderr=null).wait()
finally:
null.close()
elif WIN:
if locate:
url = _unquote_file(url)
args = ["explorer", f"/select,{url}"]
else:
args = ["start"]
if wait:
args.append("/WAIT")
args.append("")
args.append(url)
try:
return subprocess.call(args)
except OSError:
# Command not found
return 127
elif CYGWIN:
if locate:
url = _unquote_file(url)
args = ["cygstart", os.path.dirname(url)]
else:
args = ["cygstart"]
if wait:
args.append("-w")
args.append(url)
try:
return subprocess.call(args)
except OSError:
# Command not found
return 127
try:
if locate:
url = os.path.dirname(_unquote_file(url)) or "."
else:
url = _unquote_file(url)
c = subprocess.Popen(["xdg-open", url])
if wait:
return c.wait()
return 0
except OSError:
if url.startswith(("http://", "https://")) and not locate and not wait:
import webbrowser
webbrowser.open(url)
return 0
return 1
def _translate_ch_to_exc(ch: str) -> None:
if ch == "\x03":
raise KeyboardInterrupt()
if ch == "\x04" and not WIN: # Unix-like, Ctrl+D
raise EOFError()
if ch == "\x1a" and WIN: # Windows, Ctrl+Z
raise EOFError()
return None
if sys.platform == "win32":
import msvcrt
@contextlib.contextmanager
def raw_terminal() -> cabc.Iterator[int]:
yield -1
def getchar(echo: bool) -> str:
# The function `getch` will return a bytes object corresponding to
# the pressed character. Since Windows 10 build 1803, it will also
# return \x00 when called a second time after pressing a regular key.
#
# `getwch` does not share this probably-bugged behavior. Moreover, it
# returns a Unicode object by default, which is what we want.
#
# Either of these functions will return \x00 or \xe0 to indicate
# a special key, and you need to call the same function again to get
# the "rest" of the code. The fun part is that \u00e0 is
# "latin small letter a with grave", so if you type that on a French
# keyboard, you _also_ get a \xe0.
# E.g., consider the Up arrow. This returns \xe0 and then \x48. The
# resulting Unicode string reads as "a with grave" + "capital H".
# This is indistinguishable from when the user actually types
# "a with grave" and then "capital H".
#
# When \xe0 is returned, we assume it's part of a special-key sequence
# and call `getwch` again, but that means that when the user types
# the \u00e0 character, `getchar` doesn't return until a second
# character is typed.
# The alternative is returning immediately, but that would mess up
# cross-platform handling of arrow keys and others that start with
# \xe0. Another option is using `getch`, but then we can't reliably
# read non-ASCII characters, because return values of `getch` are
# limited to the current 8-bit codepage.
#
# Anyway, Click doesn't claim to do this Right(tm), and using `getwch`
# is doing the right thing in more situations than with `getch`.
if echo:
func = t.cast(t.Callable[[], str], msvcrt.getwche)
else:
func = t.cast(t.Callable[[], str], msvcrt.getwch)
rv = func()
if rv in ("\x00", "\xe0"):
# \x00 and \xe0 are control characters that indicate special key,
# see above.
rv += func()
_translate_ch_to_exc(rv)
return rv
else:
import termios
import tty
@contextlib.contextmanager
def raw_terminal() -> cabc.Iterator[int]:
f: t.TextIO | None
fd: int
if not isatty(sys.stdin):
f = open("/dev/tty")
fd = f.fileno()
else:
fd = sys.stdin.fileno()
f = None
try:
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
yield fd
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
sys.stdout.flush()
if f is not None:
f.close()
except termios.error:
pass
def getchar(echo: bool) -> str:
with raw_terminal() as fd:
ch = os.read(fd, 32).decode(get_best_encoding(sys.stdin), "replace")
if echo and isatty(sys.stdout):
sys.stdout.write(ch)
_translate_ch_to_exc(ch)
return ch

View file

@ -0,0 +1,51 @@
from __future__ import annotations
import collections.abc as cabc
import textwrap
from contextlib import contextmanager
class TextWrapper(textwrap.TextWrapper):
def _handle_long_word(
self,
reversed_chunks: list[str],
cur_line: list[str],
cur_len: int,
width: int,
) -> None:
space_left = max(width - cur_len, 1)
if self.break_long_words:
last = reversed_chunks[-1]
cut = last[:space_left]
res = last[space_left:]
cur_line.append(cut)
reversed_chunks[-1] = res
elif not cur_line:
cur_line.append(reversed_chunks.pop())
@contextmanager
def extra_indent(self, indent: str) -> cabc.Iterator[None]:
old_initial_indent = self.initial_indent
old_subsequent_indent = self.subsequent_indent
self.initial_indent += indent
self.subsequent_indent += indent
try:
yield
finally:
self.initial_indent = old_initial_indent
self.subsequent_indent = old_subsequent_indent
def indent_only(self, text: str) -> str:
rv = []
for idx, line in enumerate(text.splitlines()):
indent = self.initial_indent
if idx > 0:
indent = self.subsequent_indent
rv.append(f"{indent}{line}")
return "\n".join(rv)

Some files were not shown because too many files have changed in this diff Show more