diff --git a/CHANGES.md b/CHANGES.md index 014aec77392..25d01402a38 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ +- Format docstrings to have consistent quote placement (#2885) - `if` guards in `case` blocks are now wrapped in parentheses when the line is too long. (#4269) - Stop moving multiline strings to a new line unless inside brackets (#4289) diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index e0f45b47106..1151f4be80e 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -323,4 +323,10 @@ with contextlib.ExitStack() as exit_stack: ... ``` +### Improved docstring processing + +_Black_ will ensure docstrings are formatted consistently, by removing extra blank lines +at the beginning and end of docstrings, ensuring the opening and closing quotes are on +their own lines and collapsing docstrings with a single line of text down to one line. + (labels/preview-style)= diff --git a/src/black/__init__.py b/src/black/__init__.py index 6f0e128f56c..f08b5bfda7d 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -130,7 +130,8 @@ def from_configuration( def read_pyproject_toml( ctx: click.Context, param: click.Parameter, value: Optional[str] ) -> Optional[str]: - """Inject Black configuration from "pyproject.toml" into defaults in `ctx`. + """ + Inject Black configuration from "pyproject.toml" into defaults in `ctx`. Returns the path to a successfully found and read configuration file, None otherwise. @@ -211,7 +212,8 @@ def spellcheck_pyproject_toml_keys( def target_version_option_callback( c: click.Context, p: Union[click.Option, click.Parameter], v: Tuple[str, ...] ) -> List[TargetVersion]: - """Compute the target versions from a --target-version flag. + """ + Compute the target versions from a --target-version flag. This is its own function because mypy couldn't infer the type correctly when it was a lambda, causing mypyc trouble. @@ -227,7 +229,8 @@ def enable_unstable_feature_callback( def re_compile_maybe_verbose(regex: str) -> Pattern[str]: - """Compile a regular expression string in `regex`. + """ + Compile a regular expression string in `regex`. If it contains newlines, use verbose mode. """ @@ -823,9 +826,7 @@ def get_sources( def path_empty( src: Sized, msg: str, quiet: bool, verbose: bool, ctx: click.Context ) -> None: - """ - Exit if there is no `src` provided for formatting - """ + """Exit if there is no `src` provided for formatting""" if not src: if verbose or not quiet: out(msg) @@ -874,7 +875,8 @@ def reformat_one( *, lines: Collection[Tuple[int, int]] = (), ) -> None: - """Reformat a single file under `src` without spawning child processes. + """ + Reformat a single file under `src` without spawning child processes. `fast`, `write_back`, and `mode` options are passed to :func:`format_file_in_place` or :func:`format_stdin_to_stdout`. @@ -930,7 +932,8 @@ def format_file_in_place( *, lines: Collection[Tuple[int, int]] = (), ) -> bool: - """Format file under `src` path. Return True if changed. + """ + Format file under `src` path. Return True if changed. If `write_back` is DIFF, write a diff to stdout. If it is YES, write reformatted code to the file. @@ -997,7 +1000,8 @@ def format_stdin_to_stdout( mode: Mode, lines: Collection[Tuple[int, int]] = (), ) -> bool: - """Format file on stdin. Return True if changed. + """ + Format file on stdin. Return True if changed. If content is None, it's read from sys.stdin. @@ -1048,7 +1052,8 @@ def check_stability_and_equivalence( mode: Mode, lines: Collection[Tuple[int, int]] = (), ) -> None: - """Perform stability and equivalence checks. + """ + Perform stability and equivalence checks. Raise AssertionError if source and destination contents are not equivalent, or if a second pass of the formatter would format the @@ -1065,7 +1070,8 @@ def format_file_contents( mode: Mode, lines: Collection[Tuple[int, int]] = (), ) -> FileContent: - """Reformat contents of a file and return new contents. + """ + Reformat contents of a file and return new contents. If `fast` is False, additionally confirm that the reformatted code is valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it. @@ -1087,7 +1093,8 @@ def format_file_contents( def validate_cell(src: str, mode: Mode) -> None: - """Check that cell does not already contain TransformerManager transformations, + """ + Check that cell does not already contain TransformerManager transformations, or non-Python cell magics, which might cause tokenizer_rt to break because of indentations. @@ -1113,7 +1120,8 @@ def validate_cell(src: str, mode: Mode) -> None: def format_cell(src: str, *, fast: bool, mode: Mode) -> str: - """Format code in given cell of Jupyter notebook. + """ + Format code in given cell of Jupyter notebook. General idea is: @@ -1150,7 +1158,8 @@ def format_cell(src: str, *, fast: bool, mode: Mode) -> str: def validate_metadata(nb: MutableMapping[str, Any]) -> None: - """If notebook is marked as non-Python, don't format it. + """ + If notebook is marked as non-Python, don't format it. All notebook metadata fields are optional, see https://nbformat.readthedocs.io/en/latest/format_description.html. So @@ -1162,7 +1171,8 @@ def validate_metadata(nb: MutableMapping[str, Any]) -> None: def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileContent: - """Format Jupyter notebook. + """ + Format Jupyter notebook. Operate cell-by-cell, only on code cells, only for Python notebooks. If the ``.ipynb`` originally had a trailing newline, it'll be preserved. @@ -1196,7 +1206,8 @@ def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileCon def format_str( src_contents: str, *, mode: Mode, lines: Collection[Tuple[int, int]] = () ) -> str: - """Reformat a string and return new contents. + """ + Reformat a string and return new contents. `mode` determines formatting options, such as how many characters per line are allowed. Example: @@ -1223,7 +1234,6 @@ def f( arg: str = '', ) -> None: hey - """ if lines: lines = sanitized_lines(lines, src_contents) @@ -1292,7 +1302,8 @@ def _format_str_once( def decode_bytes(src: bytes) -> Tuple[FileContent, Encoding, NewLine]: - """Return a tuple of (decoded_contents, encoding, newline). + """ + Return a tuple of (decoded_contents, encoding, newline). `newline` is either CRLF or LF but `decoded_contents` is decoded with universal newlines (i.e. only contains LF). @@ -1311,7 +1322,8 @@ def decode_bytes(src: bytes) -> Tuple[FileContent, Encoding, NewLine]: def get_features_used( # noqa: C901 node: Node, *, future_imports: Optional[Set[str]] = None ) -> Set[Feature]: - """Return a set of (relatively) new Python features used in this file. + """ + Return a set of (relatively) new Python features used in this file. Currently looking for: - f-strings; @@ -1583,7 +1595,8 @@ def assert_stable( @contextmanager def nullcontext() -> Iterator[None]: - """Return an empty context manager. + """ + Return an empty context manager. To be used like `nullcontext` in Python 3.7. """ diff --git a/src/black/brackets.py b/src/black/brackets.py index 37e6b2590eb..14f75f62b1e 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -68,7 +68,8 @@ class BracketTracker: invisible: List[Leaf] = field(default_factory=list) def mark(self, leaf: Leaf) -> None: - """Mark `leaf` with bracket-related metadata. Keep track of delimiters. + """ + 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 @@ -139,7 +140,8 @@ def any_open_brackets(self) -> bool: 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. + """ + 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. @@ -147,7 +149,8 @@ def max_delimiter_priority(self, exclude: Iterable[LeafID] = ()) -> Priority: 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`. + """ + Return the number of delimiters with the given `priority`. If no `priority` is passed, defaults to max priority on the line. """ @@ -158,7 +161,8 @@ def delimiter_count_with_priority(self, priority: Priority = 0) -> int: 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. + """ + 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`. @@ -185,7 +189,8 @@ def maybe_decrement_after_for_loop_variable(self, leaf: Leaf) -> bool: return False def maybe_increment_lambda_arguments(self, leaf: Leaf) -> bool: - """In a lambda expression, there might be more than one argument. + """ + 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 `:`. @@ -216,7 +221,8 @@ def get_open_lsqb(self) -> Optional[Leaf]: def is_split_after_delimiter(leaf: Leaf) -> Priority: - """Return the priority of the `leaf` delimiter, given a line break after it. + """ + 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. @@ -230,7 +236,8 @@ def is_split_after_delimiter(leaf: Leaf) -> Priority: def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Priority: - """Return the priority of the `leaf` delimiter, given a line break before it. + """ + 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. @@ -326,7 +333,8 @@ def is_split_before_delimiter(leaf: Leaf, previous: Optional[Leaf] = None) -> Pr def max_delimiter_priority_in_atom(node: LN) -> Priority: - """Return maximum delimiter priority inside `node`. + """ + 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. @@ -354,7 +362,8 @@ def max_delimiter_priority_in_atom(node: LN) -> Priority: def get_leaves_inside_matching_brackets(leaves: Sequence[Leaf]) -> Set[LeafID]: - """Return leaves that are inside matching brackets. + """ + 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. diff --git a/src/black/cache.py b/src/black/cache.py index 35bddb573d2..111d9dc7f55 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -28,7 +28,8 @@ class FileData(NamedTuple): def get_cache_dir() -> Path: - """Get the cache directory used by black. + """ + 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 @@ -59,7 +60,8 @@ class Cache: @classmethod def read(cls, mode: Mode) -> Self: - """Read the cache if it exists and is well-formed. + """ + 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. diff --git a/src/black/comments.py b/src/black/comments.py index ea54e2468c9..ed314f91052 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -31,7 +31,8 @@ @dataclass class ProtoComment: - """Describes a piece of syntax that is a comment. + """ + Describes a piece of syntax that is a comment. It's not a :class:`blib2to3.pytree.Leaf` so that: @@ -50,7 +51,8 @@ class ProtoComment: def generate_comments(leaf: LN) -> Iterator[Leaf]: - """Clean the prefix of the `leaf` and generate comments from it, if any. + """ + 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 @@ -140,7 +142,8 @@ def normalize_trailing_prefix(leaf: LN, total_consumed: int) -> None: def make_comment(content: str) -> str: - """Return a consistently formatted comment from the given `content` string. + """ + 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. @@ -177,7 +180,8 @@ def normalize_fmt_off( 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. + """ + Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment. Returns True if a pair was converted. """ @@ -267,7 +271,8 @@ def convert_one_fmt_off_pair( def generate_ignored_nodes( leaf: Leaf, comment: ProtoComment, mode: Mode ) -> Iterator[LN]: - """Starting from the container of `leaf`, generate all leaves until `# fmt: on`. + """ + 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. @@ -354,7 +359,9 @@ def _generate_ignored_nodes_from_fmt_skip( def is_fmt_on(container: LN) -> bool: - """Determine whether formatting is switched on within a container. + """ + Determine whether formatting is switched on within a container. + Determined by whether the last `# fmt:` comment is `on` or `off`. """ fmt_on = False diff --git a/src/black/concurrency.py b/src/black/concurrency.py index ff0a8f5fd32..f6feac38fb3 100644 --- a/src/black/concurrency.py +++ b/src/black/concurrency.py @@ -25,7 +25,8 @@ def maybe_install_uvloop() -> None: - """If our environment has uvloop installed we use it. + """ + If our environment has uvloop installed we use it. This is called only from command-line entry points to avoid interfering with the parent process if Black is used as a library. @@ -127,7 +128,8 @@ async def schedule_formatting( loop: asyncio.AbstractEventLoop, executor: "Executor", ) -> None: - """Run formatting of `sources` in parallel using the provided `executor`. + """ + Run formatting of `sources` in parallel using the provided `executor`. (Use ProcessPoolExecutors for actual parallelism.) diff --git a/src/black/debug.py b/src/black/debug.py index cebc48765ba..476704d337e 100644 --- a/src/black/debug.py +++ b/src/black/debug.py @@ -44,7 +44,8 @@ def visit_default(self, node: LN) -> Iterator[T]: @classmethod def show(cls, code: Union[str, Leaf, Node]) -> None: - """Pretty-print the lib2to3 AST of a given string of `code`. + """ + Pretty-print the lib2to3 AST of a given string of `code`. Convenience method for debugging. """ diff --git a/src/black/files.py b/src/black/files.py index c0cadbfd890..ce6dd1234d2 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -57,7 +57,8 @@ def _cached_resolve(path: Path) -> Path: def find_project_root( srcs: Sequence[str], stdin_filename: Optional[str] = None ) -> Tuple[Path, str]: - """Return a directory containing .git, .hg, or pyproject.toml. + """ + Return a directory containing .git, .hg, or pyproject.toml. That directory will be a common parent of all files and directories passed in `srcs`. @@ -126,7 +127,8 @@ def find_pyproject_toml( @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. + """ + Parse a pyproject toml file, pulling out relevant parts for Black. If parsing fails, will raise a tomllib.TOMLDecodeError. """ @@ -145,7 +147,8 @@ def parse_pyproject_toml(path_config: str) -> Dict[str, Any]: def infer_target_version( pyproject_toml: Dict[str, Any], ) -> Optional[List[TargetVersion]]: - """Infer Black's target version from the project metadata in pyproject.toml. + """ + 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 @@ -168,7 +171,8 @@ def infer_target_version( def parse_req_python_version(requires_python: str) -> Optional[List[TargetVersion]]: - """Parse a version string (i.e. ``"3.7"``) to a list of TargetVersion. + """ + 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. @@ -183,7 +187,8 @@ def parse_req_python_version(requires_python: str) -> Optional[List[TargetVersio def parse_req_python_specifier(requires_python: str) -> Optional[List[TargetVersion]]: - """Parse a specifier string (i.e. ``">=3.7,<3.10"``) to a list of TargetVersion. + """ + 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. @@ -200,7 +205,8 @@ def parse_req_python_specifier(requires_python: str) -> Optional[List[TargetVers def strip_specifier_set(specifier_set: SpecifierSet) -> SpecifierSet: - """Strip minor versions for some specifiers in the specifier set. + """ + 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 @@ -226,7 +232,8 @@ def strip_specifier_set(specifier_set: SpecifierSet) -> SpecifierSet: @lru_cache def find_user_pyproject_toml() -> Path: - r"""Return the path to the top-level user configuration for black. + 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. @@ -337,7 +344,8 @@ def gen_python_files( verbose: bool, quiet: bool, ) -> Iterator[Path]: - """Generate all files under `path` whose paths are not excluded by the + """ + 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. diff --git a/src/black/handle_ipynb_magics.py b/src/black/handle_ipynb_magics.py index 5b2847cb0c4..d32090ea5df 100644 --- a/src/black/handle_ipynb_magics.py +++ b/src/black/handle_ipynb_magics.py @@ -65,7 +65,8 @@ def jupyter_dependencies_are_installed(*, warn: bool) -> bool: def remove_trailing_semicolon(src: str) -> Tuple[str, bool]: - """Remove trailing semicolon from Jupyter notebook cell. + """ + Remove trailing semicolon from Jupyter notebook cell. For example, @@ -97,7 +98,8 @@ def remove_trailing_semicolon(src: str) -> Tuple[str, bool]: def put_trailing_semicolon_back(src: str, has_trailing_semicolon: bool) -> str: - """Put trailing semicolon back if cell originally had it. + """ + 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. @@ -121,7 +123,8 @@ def put_trailing_semicolon_back(src: str, has_trailing_semicolon: bool) -> str: def mask_cell(src: str) -> Tuple[str, List[Replacement]]: - """Mask IPython magics so content becomes parseable Python code. + """ + Mask IPython magics so content becomes parseable Python code. For example, @@ -161,7 +164,8 @@ def mask_cell(src: str) -> Tuple[str, List[Replacement]]: def get_token(src: str, magic: str) -> str: - """Return randomly generated token to mask IPython magic with. + """ + 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 @@ -187,7 +191,8 @@ def get_token(src: str, magic: str) -> str: def replace_cell_magics(src: str) -> Tuple[str, List[Replacement]]: - """Replace cell magic with token. + """ + Replace cell magic with token. Note that 'src' will already have been processed by IPython's TransformerManager().transform_cell. @@ -218,7 +223,8 @@ def replace_cell_magics(src: str) -> Tuple[str, List[Replacement]]: def replace_magics(src: str) -> Tuple[str, List[Replacement]]: - """Replace magics within body of cell. + """ + Replace magics within body of cell. Note that 'src' will already have been processed by IPython's TransformerManager().transform_cell. @@ -259,7 +265,8 @@ def replace_magics(src: str) -> Tuple[str, List[Replacement]]: def unmask_cell(src: str, replacements: List[Replacement]) -> str: - """Remove replacements from cell. + """ + Remove replacements from cell. For example @@ -277,7 +284,8 @@ def unmask_cell(src: str, replacements: List[Replacement]) -> str: def _is_ipython_magic(node: ast.expr) -> TypeGuard[ast.Attribute]: - """Check if attribute is IPython magic. + """ + Check if attribute is IPython magic. Note that the source of the abstract syntax tree will already have been processed by IPython's @@ -314,7 +322,8 @@ def header(self) -> str: # ast.NodeVisitor + dataclass = breakage under mypyc. class CellMagicFinder(ast.NodeVisitor): - """Find cell magics. + """ + Find cell magics. Note that the source of the abstract syntax tree will already have been processed by IPython's @@ -356,7 +365,8 @@ class OffsetAndMagic: # 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. + """ + 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 @@ -378,7 +388,8 @@ 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. + """ + Look for system assign magics. For example, @@ -411,7 +422,8 @@ def visit_Assign(self, node: ast.Assign) -> None: self.generic_visit(node) def visit_Expr(self, node: ast.Expr) -> None: - """Look for magics in body of cell. + """ + Look for magics in body of cell. For examples, diff --git a/src/black/linegen.py b/src/black/linegen.py index 2d9c27a6141..d21601516a4 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -93,7 +93,8 @@ class CannotSplit(CannotTransform): # This isn't a dataclass because @dataclass + Generic breaks mypyc. # See also https://github.com/mypyc/mypyc/issues/827. class LineGenerator(Visitor[Line]): - """Generates reformatted Line objects. Empty lines are not emitted. + """ + Generates reformatted Line objects. Empty lines are not emitted. Note: destroys the tree it's visiting by mutating prefixes of its leaves in ways that will no longer stringify to valid Python code on the tree. @@ -106,7 +107,8 @@ def __init__(self, mode: Mode, features: Collection[Feature]) -> None: self.__post_init__() def line(self, indent: int = 0) -> Iterator[Line]: - """Generate a line. + """ + Generate a line. If the line is empty, only emit if it makes sense. If the line is too long, split it first and then generate. @@ -203,7 +205,8 @@ def visit_DEDENT(self, node: Leaf) -> Iterator[Line]: def visit_stmt( self, node: Node, keywords: Set[str], parens: Set[str] ) -> Iterator[Line]: - """Visit a statement. + """ + Visit a statement. This implementation is shared for `if`, `while`, `for`, `try`, `except`, `def`, `with`, `class`, `assert`, and assignments. @@ -378,7 +381,8 @@ def visit_STANDALONE_COMMENT(self, leaf: Leaf) -> Iterator[Line]: yield from self.visit_default(leaf) def visit_factor(self, node: Node) -> Iterator[Line]: - """Force parentheses between a unary op and a binary power: + """ + Force parentheses between a unary op and a binary power: -2 ** 8 -> -(2 ** 8) """ @@ -443,7 +447,7 @@ def visit_STRING(self, leaf: Leaf) -> Iterator[Line]: indent = " " * 4 * self.current_line.depth if is_multiline_string(leaf): - docstring = fix_docstring(docstring, indent) + docstring = fix_docstring(docstring, indent, preview=self.mode.preview) else: docstring = docstring.strip() @@ -548,7 +552,8 @@ def _hugging_power_ops_line_to_string( def transform_line( line: Line, mode: Mode, features: Collection[Feature] = () ) -> Iterator[Line]: - """Transform a `line`, potentially splitting it into many lines. + """ + Transform a `line`, potentially splitting it into many lines. They should fit in the allotted `line_length` but might not be able to. @@ -597,7 +602,8 @@ def transform_line( def _rhs( self: object, line: Line, features: Collection[Feature], mode: Mode ) -> Iterator[Line]: - """Wraps calls to `right_hand_split`. + """ + Wraps calls to `right_hand_split`. The calls increasingly `omit` right-hand trailers (bracket pairs with content), meaning the trailers get glued together to split on another @@ -709,7 +715,8 @@ class _BracketSplitComponent(Enum): def left_hand_split( line: Line, _features: Collection[Feature], mode: Mode ) -> Iterator[Line]: - """Split line into many lines, starting with the first matching bracket pair. + """ + Split line into many lines, starting with the first matching bracket pair. Note: this usually looks weird, only use this for function definitions. Prefer RHS otherwise. This is why this function is not symmetrical with @@ -759,7 +766,8 @@ def right_hand_split( features: Collection[Feature] = (), omit: Collection[LeafID] = (), ) -> Iterator[Line]: - """Split line into many lines, starting with the last matching bracket pair. + """ + Split line into many lines, starting with the last matching bracket pair. If the split was by optional parentheses, attempt splitting without them, too. `omit` is a collection of closing bracket IDs that shouldn't be considered for @@ -777,7 +785,8 @@ def _first_right_hand_split( line: Line, omit: Collection[LeafID] = (), ) -> RHSResult: - """Split the line into head, body, tail starting with the last bracket pair. + """ + Split the line into head, body, tail starting with the last bracket pair. Note: this function should not have side effects. It's relied upon by _maybe_split_omitting_optional_parens to get an opinion whether to prefer @@ -1001,7 +1010,8 @@ def _prefer_split_rhs_oop_over_rhs( def bracket_split_succeeded_or_raise(head: Line, body: Line, tail: Line) -> None: - """Raise :exc:`CannotSplit` if the last left- or right-hand split failed. + """ + Raise :exc:`CannotSplit` if the last left- or right-hand split failed. Do nothing otherwise. @@ -1033,7 +1043,8 @@ def bracket_split_build_line( *, component: _BracketSplitComponent, ) -> Line: - """Return a new line with given `leaves` and respective comments from `original`. + """ + Return a new line with given `leaves` and respective comments from `original`. If it's the head component, brackets will be tracked so trailing commas are respected. @@ -1101,7 +1112,8 @@ def bracket_split_build_line( def dont_increase_indentation(split_func: Transformer) -> Transformer: - """Normalize prefix of the first leaf in every line returned by `split_func`. + """ + Normalize prefix of the first leaf in every line returned by `split_func`. This is a decorator over relevant split functions. """ @@ -1151,7 +1163,8 @@ def _safe_add_trailing_comma(safe: bool, delimiter_priority: int, line: Line) -> def delimiter_split( line: Line, features: Collection[Feature], mode: Mode ) -> Iterator[Line]: - """Split according to delimiters of the highest priority. + """ + Split according to delimiters of the highest priority. If the appropriate Features are given, the split will add trailing commas also in function signatures and calls that contain `*` and `**`. @@ -1275,7 +1288,8 @@ def append_to_line(leaf: Leaf) -> Iterator[Line]: def normalize_invisible_parens( # noqa: C901 node: Node, parens_after: Set[str], *, mode: Mode, features: Collection[Feature] ) -> None: - """Make existing optional parentheses invisible or create new ones. + """ + Make existing optional parentheses invisible or create new ones. `parens_after` is a set of string leaf values immediately after which parens should be put. @@ -1437,7 +1451,8 @@ def remove_await_parens(node: Node) -> None: def _maybe_wrap_cms_in_parens( node: Node, mode: Mode, features: Collection[Feature] ) -> None: - """When enabled and safe, wrap the multiple context managers in invisible parens. + """ + When enabled and safe, wrap the multiple context managers in invisible parens. It is only safe when `features` contain Feature.PARENTHESIZED_CONTEXT_MANAGERS. """ @@ -1523,7 +1538,8 @@ def maybe_make_parens_invisible_in_atom( parent: LN, remove_brackets_around_comma: bool = False, ) -> bool: - """If it's safe, make the parens in the atom `node` invisible, recursively. + """ + If it's safe, make the parens in the atom `node` invisible, recursively. Additionally, remove repeated, adjacent invisible parens from the atom `node` as they are redundant. @@ -1624,7 +1640,8 @@ def should_split_line(line: Line, opening_bracket: Leaf) -> bool: def generate_trailers_to_omit(line: Line, line_length: int) -> Iterator[Set[LeafID]]: - """Generate sets of closing bracket IDs that should be omitted in a RHS. + """ + Generate sets of closing bracket IDs that should be omitted in a RHS. Brackets can be omitted if the entire trailer up to and including a preceding closing bracket fits in one line. diff --git a/src/black/lines.py b/src/black/lines.py index 6b65372fb3f..54666026815 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -63,7 +63,8 @@ class Line: def append( self, leaf: Leaf, preformatted: bool = False, track_bracket: bool = False ) -> None: - """Add a new `leaf` to the end of the line. + """ + Add a new `leaf` to the end of the line. Unless `preformatted` is True, the `leaf` will receive a new consistent whitespace prefix and metadata applied by :class:`BracketTracker`. @@ -97,7 +98,8 @@ def append( self.leaves.append(leaf) def append_safe(self, leaf: Leaf, preformatted: bool = False) -> None: - """Like :func:`append()` but disallow invalid standalone comment structure. + """ + Like :func:`append()` but disallow invalid standalone comment structure. Raises ValueError when any `leaf` is appended after a standalone comment or when a standalone comment is not the first leaf on the line. @@ -180,7 +182,8 @@ def is_stub_def(self) -> bool: @property def is_class_paren_empty(self) -> bool: - """Is this a class with no base classes but using parentheses? + """ + Is this a class with no base classes but using parentheses? Those are unnecessary and should be removed. """ @@ -341,7 +344,8 @@ def contains_multiline_strings(self) -> bool: return any(is_multiline_string(leaf) for leaf in self.leaves) def has_magic_trailing_comma(self, closing: Leaf) -> bool: - """Return True if we have a magic trailing comma, that is when: + """ + Return True if we have a magic trailing comma, that is when: - there's a trailing comma here - it's not from single-element square bracket indexing - it's not a one-tuple @@ -455,7 +459,8 @@ def is_complex_subscript(self, leaf: Leaf) -> bool: def enumerate_with_length( self, is_reversed: bool = False ) -> Iterator[Tuple[Index, Leaf, int]]: - """Return an enumeration of leaves with their length. + """ + Return an enumeration of leaves with their length. Stops prematurely on multiline strings and standalone comments. """ @@ -516,7 +521,8 @@ class RHSResult: @dataclass class LinesBlock: - """Class that holds information about a block of formatted lines. + """ + Class that holds information about a block of formatted lines. This is introduced so that the EmptyLineTracker can look behind the standalone comments and adjust their empty lines for class or def lines. @@ -538,7 +544,8 @@ def all_lines(self) -> List[str]: @dataclass class EmptyLineTracker: - """Provides a stateful method that returns the number of potential extra + """ + Provides a stateful method that returns the number of potential extra empty lines needed before and after the currently processed line. Note: this tracker works on lines that haven't been split yet. It assumes @@ -553,7 +560,8 @@ class EmptyLineTracker: semantic_leading_comment: Optional[LinesBlock] = None def maybe_empty_lines(self, current_line: Line) -> LinesBlock: - """Return the number of extra empty lines before and after the `current_line`. + """ + Return the number of extra empty lines before and after the `current_line`. This is for separating `def`, `async def` and `class` with extra empty lines (two on module-level). @@ -802,7 +810,8 @@ def append_leaves( def is_line_short_enough( # noqa: C901 line: Line, *, mode: Mode, line_str: str = "" ) -> bool: - """For non-multiline strings, return True if `line` is no longer than `line_length`. + """ + For non-multiline strings, return True if `line` is no longer than `line_length`. For multiline strings, looks at the context around `line` to determine if it should be inlined or split up. Uses the provided `line_str` rendering, if any, otherwise computes a new one. @@ -891,7 +900,8 @@ def is_line_short_enough( # noqa: C901 def can_be_split(line: Line) -> bool: - """Return False if the line cannot be split *for sure*. + """ + Return False if the line cannot be split *for sure*. This is not an exhaustive search but a cheap heuristic that we can use to avoid some unfortunate formattings (mostly around wrapping unsplittable code @@ -930,7 +940,8 @@ def can_omit_invisible_parens( rhs: RHSResult, line_length: int, ) -> bool: - """Does `rhs.body` have a shape safe to reformat without optional parens around it? + """ + Does `rhs.body` have a shape safe to reformat without optional parens around it? Returns True for only a subset of potentially nice looking formattings but the point is to not return false positives that end up producing lines that @@ -1066,7 +1077,8 @@ def _can_omit_closing_paren(line: Line, *, last: Leaf, line_length: int) -> bool def line_to_string(line: Line) -> str: - """Returns the string representation of @line. + """ + Returns the string representation of @line. WARNING: This is known to be computationally expensive. """ diff --git a/src/black/nodes.py b/src/black/nodes.py index c0dca6e5783..322ececc693 100644 --- a/src/black/nodes.py +++ b/src/black/nodes.py @@ -155,7 +155,8 @@ class Visitor(Generic[T]): """Basic lib2to3 visitor that yields things of type `T` on `visit()`.""" def visit(self, node: LN) -> Iterator[T]: - """Main method to visit `node` and its children. + """ + Main method to visit `node` and its children. It tries to find a `visit_*()` method for the given `node.type`, like `visit_simple_stmt` for Node objects or `visit_INDENT` for Leaf objects. @@ -186,7 +187,8 @@ def visit_default(self, node: LN) -> Iterator[T]: def whitespace(leaf: Leaf, *, complex_subscript: bool, mode: Mode) -> str: # noqa: C901 - """Return whitespace prefix if needed for the given `leaf`. + """ + Return whitespace prefix if needed for the given `leaf`. `complex_subscript` signals whether the given leaf is part of a subscription which has non-trivial arguments, like arithmetic expressions or function calls. @@ -444,10 +446,12 @@ def preceding_leaf(node: Optional[LN]) -> Optional[Leaf]: def prev_siblings_are(node: Optional[LN], tokens: List[Optional[NodeType]]) -> bool: - """Return if the `node` and its previous siblings match types against the provided + """ + Return if the `node` and its previous siblings match types against the provided list of tokens; the provided `node`has its type matched against the last element in the list. `None` can be used as the first element to declare that the start of the - list is anchored at the start of its parent's children.""" + list is anchored at the start of its parent's children. + """ if not tokens: return True if tokens[-1] is None: @@ -498,7 +502,8 @@ def replace_child(old_child: LN, new_child: LN) -> None: def container_of(leaf: Leaf) -> LN: - """Return `leaf` or one of its ancestors that is the topmost container of it. + """ + Return `leaf` or one of its ancestors that is the topmost container of it. By "container" we mean a node where `leaf` is the very first child. """ @@ -681,7 +686,8 @@ def is_simple_decorator_trailer(node: LN, last: bool = False) -> bool: def is_simple_decorator_expression(node: LN) -> bool: - """Return True iff `node` could be a 'dotted name' decorator + """ + Return True iff `node` could be a 'dotted name' decorator This function takes the node of the 'namedexpr_test' of the new decorator grammar and test if it would be valid under the old decorator grammar. @@ -726,7 +732,8 @@ def is_yield(node: LN) -> bool: def is_vararg(leaf: Leaf, within: Set[NodeType]) -> bool: - """Return True if `leaf` is a star or double star in a vararg or kwarg. + """ + Return True if `leaf` is a star or double star in a vararg or kwarg. If `within` includes VARARGS_PARENTS, this applies to function signatures. If `within` includes UNPACKING_PARENTS, it applies to right hand-side @@ -805,7 +812,8 @@ def is_stub_body(node: LN) -> bool: def is_atom_with_invisible_parens(node: LN) -> bool: - """Given a `LN`, determines whether it's an atom `node` with invisible + """ + Given a `LN`, determines whether it's an atom `node` with invisible parens. Useful in dedupe-ing and normalizing parens. """ if isinstance(node, Leaf) or node.type != syms.atom: @@ -863,7 +871,8 @@ def is_with_or_async_with_stmt(leaf: Leaf) -> bool: def is_async_stmt_or_funcdef(leaf: Leaf) -> bool: - """Return True if the given leaf starts an async def/for/with statement. + """ + Return True if the given leaf starts an async def/for/with statement. Note that `async def` can be either an `async_stmt` or `async_funcdef`, the latter is used when it has decorators. @@ -876,10 +885,12 @@ def is_async_stmt_or_funcdef(leaf: Leaf) -> bool: def is_type_comment(leaf: Leaf) -> bool: - """Return True if the given leaf is a type comment. This function should only + """ + Return True if the given leaf is a type comment. This function should only be used for general type comments (excluding ignore annotations, which should use `is_type_ignore_comment`). Note that general type comments are no longer - used in modern version of Python, this function may be deprecated in the future.""" + used in modern version of Python, this function may be deprecated in the future. + """ t = leaf.type v = leaf.value return t in {token.COMMENT, STANDALONE_COMMENT} and v.startswith("# type:") @@ -899,7 +910,8 @@ def is_type_ignore_comment_string(value: str) -> bool: def wrap_in_parentheses(parent: Node, child: LN, *, visible: bool = True) -> None: - """Wrap `child` in parentheses. + """ + Wrap `child` in parentheses. This replaces `child` with an atom holding the parentheses and the old child. That requires moving the prefix. @@ -917,9 +929,11 @@ def wrap_in_parentheses(parent: Node, child: LN, *, visible: bool = True) -> Non def unwrap_singleton_parenthesis(node: LN) -> Optional[LN]: - """Returns `wrapped` if `node` is of the shape ( wrapped ). + """ + Returns `wrapped` if `node` is of the shape ( wrapped ). - Parenthesis can be optional. Returns None otherwise""" + Parenthesis can be optional. Returns None otherwise + """ if len(node.children) != 3: return None @@ -931,7 +945,8 @@ def unwrap_singleton_parenthesis(node: LN) -> Optional[LN]: def ensure_visible(leaf: Leaf) -> None: - """Make sure parentheses are visible. + """ + Make sure parentheses are visible. They could be invisible as part of some statements (see :func:`normalize_invisible_parens` and :func:`visit_import_from`). diff --git a/src/black/numerics.py b/src/black/numerics.py index 3040de06fde..8f0acd1b901 100644 --- a/src/black/numerics.py +++ b/src/black/numerics.py @@ -6,9 +6,7 @@ def format_hex(text: str) -> str: - """ - Formats a hexadecimal string like "0x12B3" - """ + """Formats a hexadecimal string like "0x12B3" """ before, after = text[:2], text[2:] return f"{before}{after.upper()}" @@ -43,9 +41,11 @@ def format_float_or_int_string(text: str) -> str: def normalize_numeric_literal(leaf: Leaf) -> None: - """Normalizes numeric (float, int, and complex) literals. + """ + Normalizes numeric (float, int, and complex) literals. - All letters used in the representation are normalized to lowercase.""" + 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. diff --git a/src/black/report.py b/src/black/report.py index 89899f2f389..51d492f388f 100644 --- a/src/black/report.py +++ b/src/black/report.py @@ -60,7 +60,8 @@ def path_ignored(self, path: Path, message: str) -> None: @property def return_code(self) -> int: - """Return the exit code that the app should use. + """ + 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; @@ -78,7 +79,8 @@ def return_code(self) -> int: return 0 def __str__(self) -> str: - """Render a color report of the current state. + """ + Render a color report of the current state. Use `click.unstyle` to remove colors. """ diff --git a/src/black/strings.py b/src/black/strings.py index baa88162844..2ba9860c40c 100644 --- a/src/black/strings.py +++ b/src/black/strings.py @@ -26,7 +26,8 @@ def sub_twice(regex: Pattern[str], replacement: str, original: str) -> str: - """Replace `regex` with `replacement` twice on `original`. + """ + Replace `regex` with `replacement` twice on `original`. This is used by string normalization to perform replaces on overlapping matches. @@ -62,7 +63,7 @@ def lines_with_leading_tabs_expanded(s: str) -> List[str]: return lines -def fix_docstring(docstring: str, prefix: str) -> str: +def fix_docstring(docstring: str, prefix: str, *, preview: bool) -> str: # https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation if not docstring: return "" @@ -83,7 +84,19 @@ def fix_docstring(docstring: str, prefix: str) -> str: trimmed.append(prefix + stripped_line) else: trimmed.append("") - return "\n".join(trimmed) + if not preview: + return "\n".join(trimmed) + # Remove extra blank lines at the ends + if all(not line or line.isspace() for line in trimmed): + return "" + for end in (0, -1): + while not trimmed[end] or trimmed[end].isspace(): + trimmed.pop(end) + # Make single-line docstring single-lined + if len(trimmed) == 1: + return trimmed[0] + trimmed[0] = prefix + trimmed[0].strip() + return "\n".join(("", *trimmed, prefix)) def get_string_prefix(string: str) -> str: @@ -167,7 +180,8 @@ def _cached_compile(pattern: str) -> Pattern[str]: def normalize_string_quotes(s: str) -> str: - """Prefer double quotes but only if it doesn't cause more escaping. + """ + Prefer double quotes but only if it doesn't cause more escaping. Adds or removes backslashes as appropriate. Doesn't parse and fix strings nested in f-strings. diff --git a/src/black/trans.py b/src/black/trans.py index 29a978c6b71..22bb8a125cc 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -74,7 +74,8 @@ class CannotTransform(Exception): def TErr(err_msg: str) -> Err[CannotTransform]: - """(T)ransform Err + """ + (T)ransform Err Convenience function used when working with the TResult type. """ @@ -356,7 +357,8 @@ def __call__( @dataclass class CustomSplit: - """A custom (i.e. manual) string split. + """ + A custom (i.e. manual) string split. A single CustomSplit instance represents a single substring. @@ -405,7 +407,8 @@ def _get_key(string: str) -> "CustomSplitMapMixin._Key": def add_custom_splits( self, string: str, custom_splits: Iterable[CustomSplit] ) -> None: - """Custom Split Map Setter Method + """ + Custom Split Map Setter Method Side Effects: Adds a mapping from @string to the custom splits @custom_splits. @@ -414,7 +417,8 @@ def add_custom_splits( self._CUSTOM_SPLIT_MAP[key] = tuple(custom_splits) def pop_custom_splits(self, string: str) -> List[CustomSplit]: - """Custom Split Map Getter Method + """ + Custom Split Map Getter Method Returns: * A list of the custom splits that are mapped to @string, if any @@ -443,7 +447,8 @@ def has_custom_splits(self, string: str) -> bool: class StringMerger(StringTransformer, CustomSplitMapMixin): - """StringTransformer that merges strings together. + """ + StringTransformer that merges strings together. Requirements: (A) The line contains adjacent strings such that ALL of the validation checks @@ -663,7 +668,8 @@ def _merge_one_string_group( QUOTE = LL[string_idx].value[-1] def make_naked(string: str, string_prefix: str) -> str: - """Strip @string (i.e. make it a "naked" string) + """ + Strip @string (i.e. make it a "naked" string) Pre-conditions: * assert_is_leaf_string(@string) @@ -794,7 +800,8 @@ def make_naked(string: str, string_prefix: str) -> str: @staticmethod def _validate_msg(line: Line, string_idx: int) -> TResult[None]: - """Validate (M)erge (S)tring (G)roup + """ + Validate (M)erge (S)tring (G)roup Transform-time string validation logic for _merge_string_group(...). @@ -880,7 +887,8 @@ def _validate_msg(line: Line, string_idx: int) -> TResult[None]: class StringParenStripper(StringTransformer): - """StringTransformer that strips surrounding parentheses from strings. + """ + StringTransformer that strips surrounding parentheses from strings. Requirements: The line contains a string which is surrounded by parentheses and: diff --git a/tests/data/cases/comments.py b/tests/data/cases/comments.py index c34daaf6f08..fbb88e3a45a 100644 --- a/tests/data/cases/comments.py +++ b/tests/data/cases/comments.py @@ -4,7 +4,8 @@ # # Has many lines. Many, many lines. # Many, many, many lines. -"""Module docstring. +""" +Module docstring. Possibly also many, many lines. """ @@ -30,7 +31,8 @@ def function(default=None): - """Docstring comes first. + """ + Docstring comes first. Possibly many lines. """ diff --git a/tests/data/cases/comments_non_breaking_space.py b/tests/data/cases/comments_non_breaking_space.py index e17c3f4ca39..4550d8932b1 100644 --- a/tests/data/cases/comments_non_breaking_space.py +++ b/tests/data/cases/comments_non_breaking_space.py @@ -10,9 +10,10 @@ square = Square(4) # type: Optional[Square] def function(a:int=42): - """ This docstring is already formatted - a - b + """ + This docstring is already formatted + a + b """ #  There's a NBSP + 3 spaces before # And 4 spaces on the next line @@ -35,7 +36,8 @@ def function(a:int=42): def function(a: int = 42): - """This docstring is already formatted + """ + This docstring is already formatted a b """ diff --git a/tests/data/cases/docstring_newline_preview.py b/tests/data/cases/docstring_newline_preview.py index 5c129ca5f80..d0e39bd9f0f 100644 --- a/tests/data/cases/docstring_newline_preview.py +++ b/tests/data/cases/docstring_newline_preview.py @@ -2,3 +2,6 @@ """ 87 characters ............................................................................ """ + +# output +"""87 characters ............................................................................""" diff --git a/tests/data/cases/docstring_preview.py b/tests/data/cases/docstring_preview.py index a3c656be2f8..1df00a36818 100644 --- a/tests/data/cases/docstring_preview.py +++ b/tests/data/cases/docstring_preview.py @@ -1,3 +1,4 @@ +# flags: --preview def docstring_almost_at_line_limit(): """long docstring................................................................. """ @@ -63,7 +64,8 @@ def docstring_almost_at_line_limit_with_prefix(): def mulitline_docstring_almost_at_line_limit(): - """long docstring................................................................. + """ + long docstring................................................................. .................................................................................. """ @@ -85,9 +87,11 @@ def docstring_at_line_limit_with_prefix(): def multiline_docstring_at_line_limit(): - """first line----------------------------------------------------------------------- + """ + first line----------------------------------------------------------------------- - second line----------------------------------------------------------------------""" + second line---------------------------------------------------------------------- + """ def multiline_docstring_at_line_limit_with_prefix(): diff --git a/tests/data/cases/docstring_preview_2.py b/tests/data/cases/docstring_preview_2.py new file mode 100644 index 00000000000..807154df6e3 --- /dev/null +++ b/tests/data/cases/docstring_preview_2.py @@ -0,0 +1,343 @@ +# flags: --preview +class MyClass: + """ Multiline + class docstring + """ + + def method(self): + """Multiline + method docstring + """ + pass + + +def foo(): + """This is a docstring with + some lines of text here + """ + return + + +def bar(): + '''This is another docstring + with more lines of text + ''' + return + + +def baz(): + '''"This" is a string with some + embedded "quotes"''' + return + + +def troz(): + '''Indentation with tabs + is just as OK + ''' + return + + +def zort(): + """Another + multiline + docstring + """ + pass + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not +make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it! + + """ + pass + + +def this(): + r""" + 'hey ho' + """ + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ + "hey yah" """ + + +def and_this(): + ''' + "hey yah"''' + + +def multiline_whitespace(): + ''' + + + + + ''' + + +def oneline_whitespace(): + ''' ''' + + +def empty(): + """""" + + +def single_quotes(): + 'testing' + + +def believe_it_or_not_this_is_in_the_py_stdlib(): ''' +"hey yah"''' + + +def ignored_docstring(): + """a => \ +b""" + +def single_line_docstring_with_whitespace(): + """ This should be stripped """ + +def docstring_with_inline_tabs_and_space_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + + +def docstring_with_inline_tabs_and_tab_indentation(): + """hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + pass + + +def my_god_its_full_of_stars_1(): + "I'm sorry Dave\u2001" + + +# the space below is actually a \u2001, removed in output +def my_god_its_full_of_stars_2(): + "I'm sorry Dave " + + +# output + +class MyClass: + """ + Multiline + class docstring + """ + + def method(self): + """ + Multiline + method docstring + """ + pass + + +def foo(): + """ + This is a docstring with + some lines of text here + """ + return + + +def bar(): + """ + This is another docstring + with more lines of text + """ + return + + +def baz(): + """ + "This" is a string with some + embedded "quotes" + """ + return + + +def troz(): + """ + Indentation with tabs + is just as OK + """ + return + + +def zort(): + """ + Another + multiline + docstring + """ + pass + + +def poit(): + """ + Lorem ipsum dolor sit amet. + + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua + - enim ad minim veniam + - quis nostrud exercitation ullamco laboris nisi + - aliquip ex ea commodo consequat + """ + pass + + +def under_indent(): + """ + These lines are indented in a way that does not + make sense. + """ + pass + + +def over_indent(): + """ + This has a shallow indent + - But some lines are deeper + - And the closing quote is too deep + """ + pass + + +def single_line(): + """But with a newline after it!""" + pass + + +def this(): + r"""'hey ho'""" + + +def that(): + """ "hey yah" """ + + +def and_that(): + """ "hey yah" """ + + +def and_this(): + '''"hey yah"''' + + +def multiline_whitespace(): + """ """ + + +def oneline_whitespace(): + """ """ + + +def empty(): + """""" + + +def single_quotes(): + "testing" + + +def believe_it_or_not_this_is_in_the_py_stdlib(): + '''"hey yah"''' + + +def ignored_docstring(): + """a => \ +b""" + + +def single_line_docstring_with_whitespace(): + """This should be stripped""" + + +def docstring_with_inline_tabs_and_space_indentation(): + """ + hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + + +def docstring_with_inline_tabs_and_tab_indentation(): + """ + hey + + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline + mixed tabs and spaces at beginning. next line has mixed tabs and spaces only. + + line ends with some tabs + """ + pass + + +def my_god_its_full_of_stars_1(): + "I'm sorry Dave\u2001" + + +# the space below is actually a \u2001, removed in output +def my_god_its_full_of_stars_2(): + "I'm sorry Dave" diff --git a/tests/data/cases/fmtonoff.py b/tests/data/cases/fmtonoff.py index 8af94563af8..9d3c514b3d8 100644 --- a/tests/data/cases/fmtonoff.py +++ b/tests/data/cases/fmtonoff.py @@ -91,7 +91,8 @@ def example(session): .all() # fmt: on def off_and_on_without_data(): - """All comments here are technically on the same prefix. + """ + All comments here are technically on the same prefix. The comments between will be formatted. This is a known limitation. """ @@ -302,7 +303,8 @@ def example(session): def off_and_on_without_data(): - """All comments here are technically on the same prefix. + """ + All comments here are technically on the same prefix. The comments between will be formatted. This is a known limitation. """ diff --git a/tests/data/cases/module_docstring_2.py b/tests/data/cases/module_docstring_2.py index ac486096c02..078d2c2acdc 100644 --- a/tests/data/cases/module_docstring_2.py +++ b/tests/data/cases/module_docstring_2.py @@ -37,7 +37,8 @@ b = 2 # output -"""I am a very helpful module docstring. +""" +I am a very helpful module docstring. With trailing spaces (only removed with unify_docstring_detection on): Lorem ipsum dolor sit amet, consectetur adipiscing elit, diff --git a/tests/optional.py b/tests/optional.py index cbb2df7c2f8..39f83f8047a 100644 --- a/tests/optional.py +++ b/tests/optional.py @@ -63,7 +63,8 @@ def pytest_addoption(parser: "Parser") -> None: def pytest_configure(config: "Config") -> None: - """Optional tests are markers. + """ + Optional tests are markers. Use the syntax in https://docs.pytest.org/en/stable/mark.html#registering-marks. """ diff --git a/tests/test_black.py b/tests/test_black.py index 2e3ae4503f5..197d589e3aa 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1900,9 +1900,7 @@ def test_code_option_fast(self) -> None: @pytest.mark.incompatible_with_mypyc def test_code_option_config(self) -> None: - """ - Test that the code option finds the pyproject.toml in the current directory. - """ + """Test that the code option finds pyproject.toml in the current directory.""" with patch.object(black, "parse_pyproject_toml", return_value={}) as parse: args = ["--code", "print"] # This is the only directory known to contain a pyproject.toml @@ -1921,9 +1919,7 @@ def test_code_option_config(self) -> None: @pytest.mark.incompatible_with_mypyc def test_code_option_parent_config(self) -> None: - """ - Test that the code option finds the pyproject.toml in the parent directory. - """ + """Test that the code option finds pyproject.toml in the parent directory.""" with patch.object(black, "parse_pyproject_toml", return_value={}) as parse: with change_directory(THIS_DIR): args = ["--code", "print"] @@ -1940,9 +1936,7 @@ def test_code_option_parent_config(self) -> None: ), "Incorrect config loaded." def test_for_handled_unexpected_eof_error(self) -> None: - """ - Test that an unexpected EOF SyntaxError is nicely presented. - """ + """Test that an unexpected EOF SyntaxError is nicely presented.""" with pytest.raises(black.parsing.InvalidInput) as exc_info: black.lib2to3_parse("print(", {}) @@ -2959,7 +2953,8 @@ def test_equivalency_ast_parse_failure_includes_error(self) -> None: def tracefunc( frame: types.FrameType, event: str, arg: Any ) -> Callable[[types.FrameType, str, Any], Any]: - """Show function calls `from black/__init__.py` as they happen. + """ + Show function calls `from black/__init__.py` as they happen. Register this with `sys.settrace()` in a test you're debugging. """ diff --git a/tests/util.py b/tests/util.py index d5425f1f743..e67a6cd8af1 100644 --- a/tests/util.py +++ b/tests/util.py @@ -99,7 +99,8 @@ def assert_format( lines: Collection[Tuple[int, int]] = (), no_preview_line_length_1: bool = False, ) -> None: - """Convenience function to check that Black formats as expected. + """ + Convenience function to check that Black formats as expected. You can pass @minimum_version if you're passing code with newer syntax to guard safety guards so they don't just crash with a SyntaxError. Please note this is