Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add code to store comments during formatting #17

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 74 additions & 3 deletions src/pyproject_fmt/formatter/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,95 @@ def __gt__(self, __other: Any) -> bool: # noqa: U101
...


@dataclass
class TableComments:
key_comments: dict[str, list[Comment]]
top_comments: list[Comment]
bottom_comments: list[Comment]


def store_comments(table: AbstractTable) -> TableComments:
"""
Takes in a table, and returns a `TableComments` object.
All comment lines in the table are attached to the key below it.
Bottom comments are stored separately.

The only exception to this rule is: If there's comments at the top of
the table, separated from the rest of the body with newlines, then
they are stored separately as top comments.
"""
key_comments: dict[str, list[Comment]] = {}
top_comments: list[Comment] = []
bottom_comments: list[Comment] = []

body = table.value.body
first_key_index = -1
for index, (key, _) in enumerate(body):
if isinstance(key, Key):
first_key_index = index
break

if first_key_index > 0 and isinstance(body[first_key_index - 1], Whitespace):
# We may have comments at the top of the file.
for _, item in body[:first_key_index]:
if isinstance(item, Comment):
top_comments.append(item)

# delete the top comments
del body[:first_key_index]

# Start picking comments and attaching them to each key
current_comments: list[Comment] = []
for key, item in body:
if isinstance(item, Comment):
current_comments.append(item)
elif isinstance(key, Key):
key_comments[key.key] = current_comments
current_comments = []

# If we're left with comments at the bottom
if current_comments:
bottom_comments = current_comments

return TableComments(key_comments, top_comments, bottom_comments)


def insert_key_with_comments(table: AbstractTable, key: Key, item: Item, comments: TableComments) -> None:
body = table.value.body
entry_comments = comments.key_comments.get(key.key, [])
body.extend((None, comment) for comment in entry_comments)
body.append((key, item))


def order_keys(
table: AbstractTable,
to_pin: Sequence[str] | None = None,
sort_key: None | Callable[[tuple[str, tuple[Key, Item]]], SupportsDunderLT | SupportsDunderGT] = None,
) -> None:
comments = store_comments(table)

body = table.value.body
entries = {i.key: (i, v) for (i, v) in body if isinstance(i, Key)}
body.clear()

for comment in comments.top_comments:
body.append((None, comment))

for pin in to_pin or []: # push pinned to start
if pin in entries:
body.append(entries[pin])
key, value = entries[pin]
insert_key_with_comments(table, key, value, comments)
del entries[pin]
# append the rest
if sort_key is None:
body.extend(entries.values())
for _, (key, item) in entries.items():
insert_key_with_comments(table, key, item, comments)
else:
body.extend(v for k, v in sorted(entries.items(), key=sort_key))
for _, (key, item) in sorted(entries.items(), key=sort_key):
insert_key_with_comments(table, key, item, comments)

for comment in comments.bottom_comments:
body.append((None, comment))

if isinstance(table, Table):
body.append((None, Whitespace("\n"))) # add trailing newline to separate
Expand Down
10 changes: 10 additions & 0 deletions tests/formatter/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,13 @@ def test_entry_points(fmt: Fmt) -> None:
beta = {C = "c",D = "d"}
"""
fmt(fmt_project, start, expected)


def test_comments(fmt: Fmt) -> None:
start = expected = """
[project]
# A comment-only line
name = "something" # This is the name
# Another comment
"""
fmt(fmt_project, start, expected)