Remove empty line before raw dostrings

**Summary** This fixes a deviation with black where black would remove
empty lines before raw docstrings for some reason.
This commit is contained in:
konstin 2023-10-13 16:04:01 +02:00
parent 60ca6885b1
commit 1743ef8398
4 changed files with 30 additions and 20 deletions

View File

@ -102,6 +102,11 @@ class Test:
x = 1 x = 1
class EmptyLineBeforeRawDocstring:
r"""Character and line based layer over a BufferedIOBase object, buffer."""
class C(): # comment class C(): # comment
pass pass

View File

@ -496,7 +496,7 @@ impl Format<PyFormatContext<'_>> for NormalizedString<'_> {
bitflags! { bitflags! {
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(super) struct StringPrefix: u8 { pub(crate) struct StringPrefix: u8 {
const UNICODE = 0b0000_0001; const UNICODE = 0b0000_0001;
/// `r"test"` /// `r"test"`
const RAW = 0b0000_0010; const RAW = 0b0000_0010;
@ -508,7 +508,7 @@ bitflags! {
} }
impl StringPrefix { impl StringPrefix {
pub(super) fn parse(input: &str) -> StringPrefix { pub(crate) fn parse(input: &str) -> StringPrefix {
let chars = input.chars(); let chars = input.chars();
let mut prefix = StringPrefix::empty(); let mut prefix = StringPrefix::empty();
@ -533,15 +533,15 @@ impl StringPrefix {
prefix prefix
} }
pub(super) const fn text_len(self) -> TextSize { pub(crate) const fn text_len(self) -> TextSize {
TextSize::new(self.bits().count_ones()) TextSize::new(self.bits().count_ones())
} }
pub(super) const fn is_raw_string(self) -> bool { pub(crate) const fn is_raw_string(self) -> bool {
self.contains(StringPrefix::RAW) || self.contains(StringPrefix::RAW_UPPER) self.contains(StringPrefix::RAW) || self.contains(StringPrefix::RAW_UPPER)
} }
pub(super) const fn is_fstring(self) -> bool { pub(crate) const fn is_fstring(self) -> bool {
self.contains(StringPrefix::F_STRING) self.contains(StringPrefix::F_STRING)
} }
} }

View File

@ -10,7 +10,7 @@ use crate::comments::{
}; };
use crate::context::{NodeLevel, WithNodeLevel}; use crate::context::{NodeLevel, WithNodeLevel};
use crate::expression::expr_constant::ExprConstantLayout; use crate::expression::expr_constant::ExprConstantLayout;
use crate::expression::string::StringLayout; use crate::expression::string::{StringLayout, StringPrefix};
use crate::prelude::*; use crate::prelude::*;
use crate::statement::stmt_expr::FormatStmtExpr; use crate::statement::stmt_expr::FormatStmtExpr;
use crate::verbatim::{ use crate::verbatim::{
@ -99,9 +99,13 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
SuiteKind::Class => { SuiteKind::Class => {
if let Some(docstring) = DocstringStmt::try_from_statement(first) { if let Some(docstring) = DocstringStmt::try_from_statement(first) {
let prefix =
StringPrefix::parse(f.context().locator().slice(docstring.0.range()));
if !comments.has_leading(first) if !comments.has_leading(first)
&& lines_before(first.start(), source) > 1 && lines_before(first.start(), source) > 1
&& !source_type.is_stub() && !source_type.is_stub()
// For some reason black removes the empty line before raw docstrings
&& !prefix.is_raw_string()
{ {
// Allow up to one empty line before a class docstring, e.g., this is // Allow up to one empty line before a class docstring, e.g., this is
// stable formatting: // stable formatting:
@ -484,8 +488,10 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for Suite {
} }
/// A statement representing a docstring. /// A statement representing a docstring.
///
/// We keep both the outer statement and the inner constant here for convenience.
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub(crate) struct DocstringStmt<'a>(&'a Stmt); pub(crate) struct DocstringStmt<'a>(&'a Stmt, &'a ExprConstant);
impl<'a> DocstringStmt<'a> { impl<'a> DocstringStmt<'a> {
/// Checks if the statement is a simple string that can be formatted as a docstring /// Checks if the statement is a simple string that can be formatted as a docstring
@ -494,9 +500,9 @@ impl<'a> DocstringStmt<'a> {
return None; return None;
}; };
if let Expr::Constant(ExprConstant { value, .. }) = value.as_ref() { if let Expr::Constant(expr_constant @ ExprConstant { value, .. }) = value.as_ref() {
if !value.is_implicit_concatenated() { if (value.is_str() || value.is_unicode_string()) && !value.is_implicit_concatenated() {
return Some(DocstringStmt(stmt)); return Some(DocstringStmt(stmt, expr_constant));
} }
} }
@ -512,21 +518,12 @@ impl Format<PyFormatContext<'_>> for DocstringStmt<'_> {
if FormatStmtExpr.is_suppressed(node_comments.trailing, f.context()) { if FormatStmtExpr.is_suppressed(node_comments.trailing, f.context()) {
suppressed_node(self.0).fmt(f) suppressed_node(self.0).fmt(f)
} else { } else {
// SAFETY: Safe because `DocStringStmt` guarantees that it only ever wraps a `ExprStmt` containing a `ConstantExpr`.
let constant = self
.0
.as_expr_stmt()
.unwrap()
.value
.as_constant_expr()
.unwrap();
// We format the expression, but the statement carries the comments // We format the expression, but the statement carries the comments
write!( write!(
f, f,
[ [
leading_comments(node_comments.leading), leading_comments(node_comments.leading),
constant self.1
.format() .format()
.with_options(ExprConstantLayout::String(StringLayout::DocString)), .with_options(ExprConstantLayout::String(StringLayout::DocString)),
trailing_comments(node_comments.trailing), trailing_comments(node_comments.trailing),

View File

@ -108,6 +108,10 @@ class Test:
x = 1 x = 1
class EmptyLineBeforeRawDocstring:
r"""Character and line based layer over a BufferedIOBase object, buffer."""
class C(): # comment class C(): # comment
pass pass
@ -356,6 +360,10 @@ class Test:
x = 1 x = 1
class EmptyLineBeforeRawDocstring:
r"""Character and line based layer over a BufferedIOBase object, buffer."""
class C: # comment class C: # comment
pass pass