whitespace-format 0.0.3__tar.gz → 0.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {whitespace_format-0.0.3 → whitespace_format-0.0.5}/PKG-INFO +25 -24
- {whitespace_format-0.0.3 → whitespace_format-0.0.5}/README.md +23 -22
- {whitespace_format-0.0.3 → whitespace_format-0.0.5}/pyproject.toml +4 -4
- {whitespace_format-0.0.3 → whitespace_format-0.0.5}/whitespace_format.py +215 -90
- whitespace_format-0.0.3/setup.py +0 -25
- {whitespace_format-0.0.3 → whitespace_format-0.0.5}/LICENSE +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: whitespace-format
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.5
|
4
4
|
Summary: Linter and formatter for source code files and text files
|
5
5
|
Home-page: https://github.com/DavidPal/whitespace-format
|
6
6
|
License: MIT
|
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.8
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.9
|
15
15
|
Classifier: Programming Language :: Python :: 3.10
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
17
|
-
Classifier: Programming Language :: Python :: 3
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
18
18
|
Project-URL: Repository, https://github.com/DavidPal/whitespace-format
|
19
19
|
Description-Content-Type: text/markdown
|
20
20
|
|
@@ -22,7 +22,7 @@ Description-Content-Type: text/markdown
|
|
22
22
|
|
23
23
|
[](https://dl.circleci.com/status-badge/redirect/gh/DavidPal/whitespace-format/tree/main)
|
24
24
|
|
25
|
-
](https://github.com/DavidPal/whitespace-format/actions/workflows/build.yaml)
|
26
26
|
|
27
27
|
Linter and formatter for source code files and text files.
|
28
28
|
|
@@ -32,7 +32,7 @@ MarkDown, LaTeX) before checking them into a version control system.
|
|
32
32
|
|
33
33
|
The features include:
|
34
34
|
|
35
|
-
* Auto-detection of new line markers (Linux `\n`, Windows `\r\
|
35
|
+
* Auto-detection of new line markers (Linux `\n`, Windows `\r\n`, Mac `\r`).
|
36
36
|
* Add a new line marker at the end of the file if it is missing.
|
37
37
|
* Make new line markers consistent.
|
38
38
|
* Remove empty lines at the end of the file.
|
@@ -62,17 +62,18 @@ whitespace-format \
|
|
62
62
|
--normalize-new-line-markers \
|
63
63
|
foo.txt my_project/
|
64
64
|
```
|
65
|
-
The command above formats `foo.txt` and all files contained `my_project/`
|
66
|
-
its subdirectories. Files that contain `.git/` or `.idea/` in
|
67
|
-
path are excluded. For example, files in `my_project/.git/`
|
68
|
-
`my_project/.idea/` are excluded. Likewise, files ending with
|
69
|
-
excluded.
|
70
|
-
|
71
|
-
If you want
|
65
|
+
The command above formats `foo.txt` and all files contained in `my_project/`
|
66
|
+
directory and its subdirectories. Files that contain `.git/` or `.idea/` in
|
67
|
+
their (relative) path are excluded. For example, files in `my_project/.git/`
|
68
|
+
and files in `my_project/.idea/` are excluded. Likewise, files ending with
|
69
|
+
`*.pyc` are excluded.
|
70
|
+
|
71
|
+
If you want to know only if any changes **would be** made, add `--check-only`
|
72
|
+
option:
|
72
73
|
```shell
|
73
74
|
whitespace-format \
|
74
|
-
--exclude ".git/|.idea/|.pyc$" \
|
75
75
|
--check-only \
|
76
|
+
--exclude ".git/|.idea/|.pyc$" \
|
76
77
|
--new-line-marker linux \
|
77
78
|
--normalize-new-line-markers \
|
78
79
|
foo.txt my_project/
|
@@ -97,7 +98,7 @@ The regular expression is evaluated on the path of each file.
|
|
97
98
|
This option is ignored when `--add-new-line-marker-at-end-of-file` is used.
|
98
99
|
Empty lines at the end of the file are removed.
|
99
100
|
* `--normalize-new-line-markers` -- Make new line markers consistent in each file
|
100
|
-
by replacing
|
101
|
+
by replacing `\r\n`, `\n`, and `\r` with a consistent new line marker.
|
101
102
|
* `--remove-trailing-whitespace` -- Remove whitespace at the end of each line.
|
102
103
|
* `--remove-trailing-empty-lines` -- Remove empty lines at the end of each file.
|
103
104
|
* `--new-line-marker=MARKER` -- This option specifies what new line marker to use.
|
@@ -105,9 +106,9 @@ by replacing `\\r\\n`, `\\n`, and `\r` with a consistent new line marker.
|
|
105
106
|
* `auto` -- Use new line marker that is the most common in each individual file.
|
106
107
|
If no new line marker is present in the file, Linux `\n` is used.
|
107
108
|
This is the default option.
|
108
|
-
* `linux` -- Use Linux new line marker
|
109
|
-
* `mac` -- Use Mac new line marker
|
110
|
-
* `windows` -- Use Windows new line marker
|
109
|
+
* `linux` -- Use Linux new line marker `\n`.
|
110
|
+
* `mac` -- Use Mac new line marker `\r`.
|
111
|
+
* `windows` -- Use Windows new line marker `\r\n`.
|
111
112
|
* `--encoding` -- Text encoding for both reading and writing files. Default encoding is `utf-8`.
|
112
113
|
List of supported encodings can be found at
|
113
114
|
https://docs.python.org/3/library/codecs.html#standard-encodings
|
@@ -162,7 +163,7 @@ Default value is `-1`.
|
|
162
163
|
|
163
164
|
* `--normalize-non-standard-whitespace=MODE` -- Replace or remove
|
164
165
|
non-standard whitespace characters (`\v` and `\f`). `MODE` must be one of the following:
|
165
|
-
* `ignore` -- Leave `\v` and
|
166
|
+
* `ignore` -- Leave `\v` and `\f` as is. This is the default option.
|
166
167
|
* `replace` -- Replace any occurrence of `\v` or `\f` with a single space.
|
167
168
|
* `remove` -- Remove all occurrences of `\v` and `\f`
|
168
169
|
|
@@ -182,13 +183,7 @@ MIT
|
|
182
183
|
brew install poetry
|
183
184
|
```
|
184
185
|
|
185
|
-
3)
|
186
|
-
```shell
|
187
|
-
make install-python
|
188
|
-
make create-environment
|
189
|
-
```
|
190
|
-
|
191
|
-
4) Add the following lines to `.zshrc` or `.bash_profile` and restart the terminal:
|
186
|
+
3) Add the following lines to `.zshrc` or `.bash_profile` and restart the terminal:
|
192
187
|
```shell
|
193
188
|
# Pyenv settings
|
194
189
|
export PYENV_ROOT="$HOME/.pyenv"
|
@@ -197,6 +192,12 @@ MIT
|
|
197
192
|
eval "$(pyenv virtualenv-init -)"
|
198
193
|
```
|
199
194
|
|
195
|
+
4) Create Python virtual environment with the correct Python version:
|
196
|
+
```shell
|
197
|
+
make install-python
|
198
|
+
make create-environment
|
199
|
+
```
|
200
|
+
|
200
201
|
5) Install all dependencies
|
201
202
|
```shell
|
202
203
|
make install-dependecies
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
[](https://dl.circleci.com/status-badge/redirect/gh/DavidPal/whitespace-format/tree/main)
|
4
4
|
|
5
|
-
](https://github.com/DavidPal/whitespace-format/actions/workflows/build.yaml)
|
6
6
|
|
7
7
|
Linter and formatter for source code files and text files.
|
8
8
|
|
@@ -12,7 +12,7 @@ MarkDown, LaTeX) before checking them into a version control system.
|
|
12
12
|
|
13
13
|
The features include:
|
14
14
|
|
15
|
-
* Auto-detection of new line markers (Linux `\n`, Windows `\r\
|
15
|
+
* Auto-detection of new line markers (Linux `\n`, Windows `\r\n`, Mac `\r`).
|
16
16
|
* Add a new line marker at the end of the file if it is missing.
|
17
17
|
* Make new line markers consistent.
|
18
18
|
* Remove empty lines at the end of the file.
|
@@ -42,17 +42,18 @@ whitespace-format \
|
|
42
42
|
--normalize-new-line-markers \
|
43
43
|
foo.txt my_project/
|
44
44
|
```
|
45
|
-
The command above formats `foo.txt` and all files contained `my_project/`
|
46
|
-
its subdirectories. Files that contain `.git/` or `.idea/` in
|
47
|
-
path are excluded. For example, files in `my_project/.git/`
|
48
|
-
`my_project/.idea/` are excluded. Likewise, files ending with
|
49
|
-
excluded.
|
50
|
-
|
51
|
-
If you want
|
45
|
+
The command above formats `foo.txt` and all files contained in `my_project/`
|
46
|
+
directory and its subdirectories. Files that contain `.git/` or `.idea/` in
|
47
|
+
their (relative) path are excluded. For example, files in `my_project/.git/`
|
48
|
+
and files in `my_project/.idea/` are excluded. Likewise, files ending with
|
49
|
+
`*.pyc` are excluded.
|
50
|
+
|
51
|
+
If you want to know only if any changes **would be** made, add `--check-only`
|
52
|
+
option:
|
52
53
|
```shell
|
53
54
|
whitespace-format \
|
54
|
-
--exclude ".git/|.idea/|.pyc$" \
|
55
55
|
--check-only \
|
56
|
+
--exclude ".git/|.idea/|.pyc$" \
|
56
57
|
--new-line-marker linux \
|
57
58
|
--normalize-new-line-markers \
|
58
59
|
foo.txt my_project/
|
@@ -77,7 +78,7 @@ The regular expression is evaluated on the path of each file.
|
|
77
78
|
This option is ignored when `--add-new-line-marker-at-end-of-file` is used.
|
78
79
|
Empty lines at the end of the file are removed.
|
79
80
|
* `--normalize-new-line-markers` -- Make new line markers consistent in each file
|
80
|
-
by replacing
|
81
|
+
by replacing `\r\n`, `\n`, and `\r` with a consistent new line marker.
|
81
82
|
* `--remove-trailing-whitespace` -- Remove whitespace at the end of each line.
|
82
83
|
* `--remove-trailing-empty-lines` -- Remove empty lines at the end of each file.
|
83
84
|
* `--new-line-marker=MARKER` -- This option specifies what new line marker to use.
|
@@ -85,9 +86,9 @@ by replacing `\\r\\n`, `\\n`, and `\r` with a consistent new line marker.
|
|
85
86
|
* `auto` -- Use new line marker that is the most common in each individual file.
|
86
87
|
If no new line marker is present in the file, Linux `\n` is used.
|
87
88
|
This is the default option.
|
88
|
-
* `linux` -- Use Linux new line marker
|
89
|
-
* `mac` -- Use Mac new line marker
|
90
|
-
* `windows` -- Use Windows new line marker
|
89
|
+
* `linux` -- Use Linux new line marker `\n`.
|
90
|
+
* `mac` -- Use Mac new line marker `\r`.
|
91
|
+
* `windows` -- Use Windows new line marker `\r\n`.
|
91
92
|
* `--encoding` -- Text encoding for both reading and writing files. Default encoding is `utf-8`.
|
92
93
|
List of supported encodings can be found at
|
93
94
|
https://docs.python.org/3/library/codecs.html#standard-encodings
|
@@ -142,7 +143,7 @@ Default value is `-1`.
|
|
142
143
|
|
143
144
|
* `--normalize-non-standard-whitespace=MODE` -- Replace or remove
|
144
145
|
non-standard whitespace characters (`\v` and `\f`). `MODE` must be one of the following:
|
145
|
-
* `ignore` -- Leave `\v` and
|
146
|
+
* `ignore` -- Leave `\v` and `\f` as is. This is the default option.
|
146
147
|
* `replace` -- Replace any occurrence of `\v` or `\f` with a single space.
|
147
148
|
* `remove` -- Remove all occurrences of `\v` and `\f`
|
148
149
|
|
@@ -162,13 +163,7 @@ MIT
|
|
162
163
|
brew install poetry
|
163
164
|
```
|
164
165
|
|
165
|
-
3)
|
166
|
-
```shell
|
167
|
-
make install-python
|
168
|
-
make create-environment
|
169
|
-
```
|
170
|
-
|
171
|
-
4) Add the following lines to `.zshrc` or `.bash_profile` and restart the terminal:
|
166
|
+
3) Add the following lines to `.zshrc` or `.bash_profile` and restart the terminal:
|
172
167
|
```shell
|
173
168
|
# Pyenv settings
|
174
169
|
export PYENV_ROOT="$HOME/.pyenv"
|
@@ -177,6 +172,12 @@ MIT
|
|
177
172
|
eval "$(pyenv virtualenv-init -)"
|
178
173
|
```
|
179
174
|
|
175
|
+
4) Create Python virtual environment with the correct Python version:
|
176
|
+
```shell
|
177
|
+
make install-python
|
178
|
+
make create-environment
|
179
|
+
```
|
180
|
+
|
180
181
|
5) Install all dependencies
|
181
182
|
```shell
|
182
183
|
make install-dependecies
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "whitespace-format"
|
3
|
-
version = "0.0.
|
3
|
+
version = "0.0.5"
|
4
4
|
description = "Linter and formatter for source code files and text files"
|
5
5
|
license = "MIT"
|
6
6
|
authors = ["David Pal <davidko.pal@gmail.com>"]
|
@@ -25,10 +25,10 @@ coverage = "^7.2.4"
|
|
25
25
|
flake8 = "^5.0.4"
|
26
26
|
flake8-absolute-import = "^1.0.0.1"
|
27
27
|
isort = {extras = ["colors"], version = "^5.11.5"}
|
28
|
-
mypy = "^1.
|
28
|
+
mypy = "^1.4.1"
|
29
29
|
pydocstyle = {extras = ["toml"], version = "^6.3.0"}
|
30
|
-
pytest = "^7.
|
31
|
-
pylint = "^2.
|
30
|
+
pytest = "^7.4.1"
|
31
|
+
pylint = "^2.17.5"
|
32
32
|
pylint-quotes = "^0.2.3"
|
33
33
|
|
34
34
|
[build-system]
|
@@ -10,22 +10,26 @@ Usage:
|
|
10
10
|
python whitespace_format.py [OPTIONS] [FILES ...]
|
11
11
|
"""
|
12
12
|
|
13
|
+
from __future__ import annotations
|
14
|
+
|
13
15
|
import argparse
|
16
|
+
import copy
|
14
17
|
import dataclasses
|
15
18
|
import pathlib
|
16
19
|
import re
|
17
20
|
import sys
|
18
21
|
from typing import Callable
|
22
|
+
from typing import Dict
|
19
23
|
from typing import List
|
20
24
|
|
21
|
-
VERSION = "0.0.
|
25
|
+
VERSION = "0.0.5"
|
22
26
|
|
23
27
|
# Regular expression that does NOT match any string.
|
24
28
|
UNMATCHABLE_REGEX = "$."
|
25
29
|
|
26
|
-
|
27
|
-
"linux": "\n",
|
30
|
+
END_OF_LINE_MARKERS = {
|
28
31
|
"windows": "\r\n",
|
32
|
+
"linux": "\n",
|
29
33
|
"mac": "\r",
|
30
34
|
}
|
31
35
|
|
@@ -68,6 +72,14 @@ def color_print(message: str, parsed_arguments: argparse.Namespace):
|
|
68
72
|
print(message)
|
69
73
|
|
70
74
|
|
75
|
+
def string_to_hex(text: str) -> str:
|
76
|
+
"""Converts a string into a human-readable hexadecimal representation.
|
77
|
+
|
78
|
+
This function is for debugging purposes only. It is used only during development.
|
79
|
+
"""
|
80
|
+
return ":".join(f"{ord(character):02x}" for character in text)
|
81
|
+
|
82
|
+
|
71
83
|
def die(error_code: int, message: str = ""):
|
72
84
|
"""Exits the script."""
|
73
85
|
if message:
|
@@ -78,7 +90,7 @@ def die(error_code: int, message: str = ""):
|
|
78
90
|
def read_file_content(file_name: str, encoding: str) -> str:
|
79
91
|
"""Reads content of a file."""
|
80
92
|
try:
|
81
|
-
with open(file_name, "r", encoding=encoding) as file:
|
93
|
+
with open(file_name, "r", encoding=encoding, newline="") as file:
|
82
94
|
return file.read()
|
83
95
|
except IOError as exception:
|
84
96
|
die(2, f"Cannot read file '{file_name}': {exception}")
|
@@ -96,99 +108,192 @@ def write_file(file_name: str, file_content: str, encoding: str):
|
|
96
108
|
die(4, f"Cannot write to file '{file_name}': {exception}")
|
97
109
|
|
98
110
|
|
99
|
-
|
100
|
-
|
111
|
+
@dataclasses.dataclass
|
112
|
+
class Line:
|
113
|
+
"""Line of a text file.
|
101
114
|
|
102
|
-
The
|
115
|
+
The line is split into two parts:
|
116
|
+
1) Content
|
117
|
+
2) End of line marker ("\n", or "\r", or "\r\n")
|
103
118
|
"""
|
104
|
-
windows_count = file_content.count("\r\n")
|
105
|
-
linux_count = file_content.count("\n") - windows_count
|
106
|
-
mac_count = file_content.count("\r") - windows_count
|
107
|
-
|
108
|
-
# Pick the new line marker with the highest count.
|
109
|
-
# Break ties according to the ordering: Linux > Windows > Mac.
|
110
|
-
_, _, new_line_marker_guess = max(
|
111
|
-
(linux_count, 3, "\n"),
|
112
|
-
(windows_count, 2, "\r\n"),
|
113
|
-
(mac_count, 1, "\r"),
|
114
|
-
)
|
115
119
|
|
116
|
-
|
120
|
+
content: str
|
121
|
+
end_of_line_marker: str
|
117
122
|
|
123
|
+
@staticmethod
|
124
|
+
def create_from_string(line: str) -> Line:
|
125
|
+
"""Creates a line from a string.
|
118
126
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
return
|
125
|
-
if file_content[i : i + 2] == "\r\n":
|
126
|
-
return file_content[: i + 2]
|
127
|
-
return file_content[: i + 1]
|
127
|
+
The function splits the input into content and end_of_line_marker.
|
128
|
+
"""
|
129
|
+
for end_of_line_marker in ["\r\n", "\n", "\r"]:
|
130
|
+
if line.endswith(end_of_line_marker):
|
131
|
+
return Line(line[: -len(end_of_line_marker)], end_of_line_marker)
|
132
|
+
return Line(line, "")
|
128
133
|
|
134
|
+
def to_hex(self):
|
135
|
+
"""Returns a human-readable hexadecimal representation of the line.
|
129
136
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
137
|
+
This function is for debugging purposes only. It is used only during development.
|
138
|
+
"""
|
139
|
+
return f"({string_to_hex(self.content)}, {string_to_hex(self.end_of_line_marker)})"
|
140
|
+
|
141
|
+
|
142
|
+
def split_lines(text: str) -> List[Line]:
|
143
|
+
"""Splits a string into lines."""
|
144
|
+
lines: List[Line] = []
|
145
|
+
current_line = ""
|
146
|
+
for i, char in enumerate(text):
|
147
|
+
current_line += char
|
148
|
+
if (char == "\n") or (
|
149
|
+
(char == "\r") and ((i >= len(text) - 1) or (not text[i + 1] == "\n"))
|
150
|
+
):
|
151
|
+
lines.append(Line.create_from_string(current_line))
|
152
|
+
current_line = ""
|
153
|
+
|
154
|
+
if current_line:
|
155
|
+
lines.append(Line.create_from_string(current_line))
|
136
156
|
|
157
|
+
return lines
|
158
|
+
|
159
|
+
|
160
|
+
def concatenate_lines(lines: List[Line]) -> str:
|
161
|
+
"""Concatenates a list of lines into a single string including end-of-line markers."""
|
162
|
+
return "".join(line.content + line.end_of_line_marker for line in lines)
|
163
|
+
|
164
|
+
|
165
|
+
def guess_end_of_line_marker(lines: List[Line]) -> str:
|
166
|
+
"""Guesses the end of line marker.
|
167
|
+
|
168
|
+
The function returns the most common end-of-line marker.
|
169
|
+
Ties are broken in order Linux "\n", Mac "\r", Windows "\r\n".
|
170
|
+
If no end-of-line marker is present, default to the Linux "\n" end-of-line marker.
|
171
|
+
"""
|
172
|
+
counts: Dict[str, int] = {"\n": 0, "\r": 0, "\r\n": 0}
|
173
|
+
for line in lines:
|
174
|
+
if line.end_of_line_marker in counts:
|
175
|
+
counts[line.end_of_line_marker] += 1
|
176
|
+
max_count = max(counts.values())
|
177
|
+
for end_of_line_marker, count in counts.items():
|
178
|
+
if count == max_count:
|
179
|
+
return end_of_line_marker
|
180
|
+
return "\n" # This return statement is never executed.
|
181
|
+
|
182
|
+
|
183
|
+
def remove_trailing_empty_lines(lines: List[Line]) -> List[Line]:
|
184
|
+
"""Removes trailing empty lines.
|
185
|
+
|
186
|
+
If there are no lines, empty list is returned.
|
187
|
+
If all lines are empty, the first line is kept.
|
188
|
+
"""
|
189
|
+
num_empty_trailing_lines = 0
|
190
|
+
while (num_empty_trailing_lines < len(lines) - 1) and (
|
191
|
+
not lines[-num_empty_trailing_lines - 1].content
|
192
|
+
):
|
193
|
+
num_empty_trailing_lines += 1
|
194
|
+
return copy.deepcopy(lines[: len(lines) - num_empty_trailing_lines])
|
195
|
+
|
196
|
+
|
197
|
+
def remove_dummy_lines(lines: List[Line]) -> List[Line]:
|
198
|
+
"""Remove empty lines that also have empty end-of-line markers."""
|
199
|
+
return [line for line in lines if line.content or line.end_of_line_marker]
|
200
|
+
|
201
|
+
|
202
|
+
def remove_trailing_whitespace(lines: List[Line]) -> List[Line]:
|
203
|
+
"""Removes trailing whitespace from every line."""
|
204
|
+
lines = [
|
205
|
+
Line(
|
206
|
+
re.sub(r"[ \n\r\t\f\v]*$", "", line.content),
|
207
|
+
line.end_of_line_marker,
|
208
|
+
)
|
209
|
+
for line in lines
|
210
|
+
]
|
211
|
+
return remove_dummy_lines(lines)
|
137
212
|
|
138
|
-
def remove_all_new_line_marker_from_end_of_file(file_content: str) -> str:
|
139
|
-
"""Removes all new line markers from the end of the file."""
|
140
|
-
return file_content.rstrip("\n\r")
|
141
213
|
|
214
|
+
def normalize_end_of_line_markers(lines: List[Line], new_end_of_line_marker: str) -> List[Line]:
|
215
|
+
"""Replaces end-of-line marker in all lines with a new end-of-line marker.
|
142
216
|
|
143
|
-
|
144
|
-
"""
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
return file_content
|
217
|
+
Lines without end-of-line markers (i.e. possibly the last line) are left unchanged.
|
218
|
+
"""
|
219
|
+
return [
|
220
|
+
Line(line.content, new_end_of_line_marker) if line.end_of_line_marker else line
|
221
|
+
for line in lines
|
222
|
+
]
|
150
223
|
|
151
224
|
|
152
|
-
def
|
153
|
-
"""
|
154
|
-
|
225
|
+
def remove_all_end_of_line_markers_from_end_of_file(lines: List[Line]) -> List[Line]:
|
226
|
+
"""Removes all end-of-line markers from the end of the file."""
|
227
|
+
lines = remove_trailing_empty_lines(lines)
|
228
|
+
if not lines:
|
229
|
+
return []
|
230
|
+
lines[-1] = Line(lines[-1].content, "")
|
231
|
+
return remove_dummy_lines(lines)
|
155
232
|
|
156
233
|
|
157
|
-
def
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
234
|
+
def add_end_of_line_marker_at_end_of_file(
|
235
|
+
lines: List[Line], new_end_of_line_marker: str
|
236
|
+
) -> List[Line]:
|
237
|
+
"""Adds new end-of-line marker to the end of file if it is missing."""
|
238
|
+
if not lines:
|
239
|
+
return [Line("", new_end_of_line_marker)]
|
240
|
+
lines = copy.deepcopy(lines)
|
241
|
+
lines[-1] = Line(lines[-1].content, new_end_of_line_marker)
|
242
|
+
return lines
|
162
243
|
|
163
244
|
|
164
|
-
def normalize_empty_file(
|
245
|
+
def normalize_empty_file(lines: List[Line], mode: str, new_end_of_line_marker: str) -> List[Line]:
|
165
246
|
"""Replaces file with an empty file."""
|
166
247
|
if mode == "empty":
|
167
|
-
return
|
248
|
+
return []
|
168
249
|
if mode == "one-line":
|
169
|
-
return
|
170
|
-
return
|
250
|
+
return [Line("", new_end_of_line_marker)]
|
251
|
+
return copy.deepcopy(lines)
|
171
252
|
|
172
253
|
|
173
|
-
def is_whitespace_only(
|
254
|
+
def is_whitespace_only(lines: List[Line]) -> bool:
|
174
255
|
"""Determines if file consists only of whitespace."""
|
175
|
-
|
256
|
+
for line in lines:
|
257
|
+
if line.content.strip(" \n\r\t\v\f"):
|
258
|
+
return False
|
259
|
+
return True
|
176
260
|
|
177
261
|
|
178
|
-
def normalize_non_standard_whitespace(
|
262
|
+
def normalize_non_standard_whitespace(lines: List[Line], mode: str) -> List[Line]:
|
179
263
|
"""Removes non-standard whitespace characters."""
|
180
264
|
if mode == "ignore":
|
181
|
-
return
|
265
|
+
return copy.deepcopy(lines)
|
182
266
|
if mode == "replace":
|
183
|
-
return
|
184
|
-
|
267
|
+
return [
|
268
|
+
Line(line.content.translate(str.maketrans("\v\f", " ", "")), line.end_of_line_marker)
|
269
|
+
for line in lines
|
270
|
+
]
|
271
|
+
return [
|
272
|
+
Line(line.content.translate(str.maketrans("", "", "\v\f")), line.end_of_line_marker)
|
273
|
+
for line in lines
|
274
|
+
]
|
185
275
|
|
186
276
|
|
187
|
-
def replace_tabs_with_spaces(
|
277
|
+
def replace_tabs_with_spaces(lines: List[Line], num_spaces: int) -> List[Line]:
|
188
278
|
"""Replaces tabs with spaces."""
|
189
279
|
if num_spaces < 0:
|
190
|
-
return
|
191
|
-
return
|
280
|
+
return copy.deepcopy(lines)
|
281
|
+
return [
|
282
|
+
Line(line.content.replace("\t", num_spaces * " "), line.end_of_line_marker)
|
283
|
+
for line in lines
|
284
|
+
]
|
285
|
+
|
286
|
+
|
287
|
+
def compute_difference(original_lines: List[Line], new_lines: List[Line]) -> List[int]:
|
288
|
+
"""Computes the indices of lines that differ."""
|
289
|
+
line_numbers = [
|
290
|
+
line_number
|
291
|
+
for line_number, (original_line, new_line) in enumerate(zip(original_lines, new_lines))
|
292
|
+
if not original_line == new_line
|
293
|
+
]
|
294
|
+
if len(original_lines) != len(new_lines):
|
295
|
+
line_numbers.append(min(len(original_lines), len(new_lines)))
|
296
|
+
return line_numbers
|
192
297
|
|
193
298
|
|
194
299
|
@dataclasses.dataclass
|
@@ -199,25 +304,36 @@ class ChangeDescription:
|
|
199
304
|
change: str
|
200
305
|
|
201
306
|
|
307
|
+
@dataclasses.dataclass
|
308
|
+
class LineChange:
|
309
|
+
"""Description of a change on a particular line."""
|
310
|
+
|
311
|
+
check_only: str
|
312
|
+
change: str
|
313
|
+
line_number: int
|
314
|
+
|
315
|
+
|
202
316
|
class FileContentTracker:
|
203
317
|
"""Tracks changes of the content of a file as it undergoes formatting."""
|
204
318
|
|
205
|
-
def __init__(self,
|
319
|
+
def __init__(self, lines: List[Line]):
|
206
320
|
"""Initializes an instance of the file content tracker."""
|
207
|
-
self.
|
208
|
-
self.
|
209
|
-
self.
|
321
|
+
self.initial_lines = lines
|
322
|
+
self.lines = copy.deepcopy(lines)
|
323
|
+
self.line_changes: List[LineChange] = []
|
210
324
|
|
211
|
-
def format(self, change: ChangeDescription, function: Callable[...,
|
325
|
+
def format(self, change: ChangeDescription, function: Callable[..., List[Line]], *args):
|
212
326
|
"""Applies a change to the content of the file."""
|
213
|
-
previous_content = self.
|
214
|
-
self.
|
215
|
-
if previous_content != self.
|
216
|
-
self.
|
327
|
+
previous_content = self.lines
|
328
|
+
self.lines = function(self.lines, *args)
|
329
|
+
if previous_content != self.lines:
|
330
|
+
line_numbers = compute_difference(previous_content, self.lines)
|
331
|
+
for line_number in line_numbers:
|
332
|
+
self.line_changes.append(LineChange(change.check_only, change.change, line_number))
|
217
333
|
|
218
334
|
def is_changed(self) -> bool:
|
219
335
|
"""Determines if the file content has changed."""
|
220
|
-
return self.
|
336
|
+
return self.lines != self.initial_lines
|
221
337
|
|
222
338
|
|
223
339
|
def format_file_content(
|
@@ -225,12 +341,12 @@ def format_file_content(
|
|
225
341
|
parsed_arguments: argparse.Namespace,
|
226
342
|
):
|
227
343
|
"""Formats the content of file represented as a string."""
|
228
|
-
new_line_marker =
|
344
|
+
new_line_marker = END_OF_LINE_MARKERS.get(
|
229
345
|
parsed_arguments.new_line_marker,
|
230
|
-
|
346
|
+
guess_end_of_line_marker(file_content_tracker.initial_lines),
|
231
347
|
)
|
232
348
|
|
233
|
-
if is_whitespace_only(file_content_tracker.
|
349
|
+
if is_whitespace_only(file_content_tracker.initial_lines):
|
234
350
|
changes = {
|
235
351
|
"ignore": ChangeDescription("", ""),
|
236
352
|
"empty": ChangeDescription(
|
@@ -246,7 +362,7 @@ def format_file_content(
|
|
246
362
|
),
|
247
363
|
),
|
248
364
|
}
|
249
|
-
if not file_content_tracker.
|
365
|
+
if not file_content_tracker.initial_lines:
|
250
366
|
file_content_tracker.format(
|
251
367
|
changes[parsed_arguments.normalize_empty_files],
|
252
368
|
normalize_empty_file,
|
@@ -265,8 +381,8 @@ def format_file_content(
|
|
265
381
|
if parsed_arguments.remove_trailing_whitespace:
|
266
382
|
file_content_tracker.format(
|
267
383
|
ChangeDescription(
|
268
|
-
check_only="Whitespace at the end of line
|
269
|
-
change="Whitespace at the end of line
|
384
|
+
check_only="Whitespace at the end of line needs to be removed.",
|
385
|
+
change="Whitespace at the end of line was removed.",
|
270
386
|
),
|
271
387
|
remove_trailing_whitespace,
|
272
388
|
)
|
@@ -308,7 +424,7 @@ def format_file_content(
|
|
308
424
|
),
|
309
425
|
change=f"New line marker(s) were replaced with {repr(new_line_marker)}.",
|
310
426
|
),
|
311
|
-
|
427
|
+
normalize_end_of_line_markers,
|
312
428
|
new_line_marker,
|
313
429
|
)
|
314
430
|
|
@@ -320,7 +436,7 @@ def format_file_content(
|
|
320
436
|
change=f"New line marker was added to the end of the file, "
|
321
437
|
f"or replaced with {repr(new_line_marker)}.",
|
322
438
|
),
|
323
|
-
|
439
|
+
add_end_of_line_marker_at_end_of_file,
|
324
440
|
new_line_marker,
|
325
441
|
)
|
326
442
|
elif parsed_arguments.remove_new_line_marker_from_end_of_file:
|
@@ -329,14 +445,15 @@ def format_file_content(
|
|
329
445
|
check_only="New line marker(s) need to removed from the end of the file.",
|
330
446
|
change="New line marker(s) were removed from the end of the file.",
|
331
447
|
),
|
332
|
-
|
448
|
+
remove_all_end_of_line_markers_from_end_of_file,
|
333
449
|
)
|
334
450
|
|
335
451
|
|
336
452
|
def reformat_file(file_name: str, parsed_arguments: argparse.Namespace) -> bool:
|
337
453
|
"""Reformats a file."""
|
338
454
|
file_content = read_file_content(file_name, parsed_arguments.encoding)
|
339
|
-
|
455
|
+
lines = split_lines(file_content)
|
456
|
+
file_content_tracker = FileContentTracker(lines)
|
340
457
|
format_file_content(file_content_tracker, parsed_arguments)
|
341
458
|
is_changed = file_content_tracker.is_changed()
|
342
459
|
if parsed_arguments.verbose:
|
@@ -348,9 +465,11 @@ def reformat_file(file_name: str, parsed_arguments: argparse.Namespace) -> bool:
|
|
348
465
|
f"[RED]needs to be formatted[RESET_ALL]",
|
349
466
|
parsed_arguments,
|
350
467
|
)
|
351
|
-
for
|
468
|
+
for line_change in file_content_tracker.line_changes:
|
352
469
|
color_print(
|
353
|
-
f" [BOLD][BLUE]↳
|
470
|
+
f" [BOLD][BLUE]↳ line {line_change.line_number + 1}: "
|
471
|
+
f"[WHITE]{line_change.check_only}[RESET_ALL]",
|
472
|
+
parsed_arguments,
|
354
473
|
)
|
355
474
|
else:
|
356
475
|
if parsed_arguments.verbose:
|
@@ -362,9 +481,15 @@ def reformat_file(file_name: str, parsed_arguments: argparse.Namespace) -> bool:
|
|
362
481
|
else:
|
363
482
|
if is_changed:
|
364
483
|
color_print(f"[WHITE]Reformatted [BOLD]{file_name}[RESET_ALL]", parsed_arguments)
|
365
|
-
for
|
366
|
-
color_print(
|
367
|
-
|
484
|
+
for line_change in file_content_tracker.line_changes:
|
485
|
+
color_print(
|
486
|
+
f" [BOLD][BLUE]↳ line {line_change.line_number + 1}: "
|
487
|
+
f"[WHITE]{line_change.change}[RESET_ALL]",
|
488
|
+
parsed_arguments,
|
489
|
+
)
|
490
|
+
write_file(
|
491
|
+
file_name, concatenate_lines(file_content_tracker.lines), parsed_arguments.encoding
|
492
|
+
)
|
368
493
|
else:
|
369
494
|
if parsed_arguments.verbose:
|
370
495
|
color_print(f"[WHITE]{file_name} [BLUE]left unchanged[RESET_ALL]", parsed_arguments)
|
whitespace_format-0.0.3/setup.py
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
from setuptools import setup
|
3
|
-
|
4
|
-
modules = \
|
5
|
-
['whitespace_format']
|
6
|
-
entry_points = \
|
7
|
-
{'console_scripts': ['whitespace-format = whitespace_format:main']}
|
8
|
-
|
9
|
-
setup_kwargs = {
|
10
|
-
'name': 'whitespace-format',
|
11
|
-
'version': '0.0.3',
|
12
|
-
'description': 'Linter and formatter for source code files and text files',
|
13
|
-
'long_description': '# whitespace-format\n\n[](https://dl.circleci.com/status-badge/redirect/gh/DavidPal/whitespace-format/tree/main)\n\n\n\nLinter and formatter for source code files and text files.\n\nThe purpose of this tool is to normalize source code files (e.g. Python, Java,\nC/C++, Ruby, Go, JavaScript, etc.) and text files (HTML, JSON, YAML, CSV,\nMarkDown, LaTeX) before checking them into a version control system.\n\nThe features include:\n\n* Auto-detection of new line markers (Linux `\\n`, Windows `\\r\\r`, Mac `\\r`).\n* Add a new line marker at the end of the file if it is missing.\n* Make new line markers consistent.\n* Remove empty lines at the end of the file.\n* Remove whitespace at the end of each line.\n* Replace tabs with spaces.\n* Remove/replace non-standard whitespace characters.\n\nThe formatting changes are\n[idempotent](https://en.wikipedia.org/wiki/Idempotence), i.e., running the tool\nsecond time (with the same parameters) has no effect.\n\n## Installation\n\n```shell\npip install whitespace-format\n```\n\nInstallation requires Python 3.7.5 or higher.\n\n## Usage\n\nA sample command that formats source code files:\n```shell\nwhitespace-format \\\n --exclude ".git/|.idea/|.pyc$" \\\n --new-line-marker linux \\\n --normalize-new-line-markers \\\n foo.txt my_project/\n```\nThe command above formats `foo.txt` and all files contained `my_project/` and\nits subdirectories. Files that contain `.git/` or `.idea/` in their (relative)\npath are excluded. For example, files in `my_project/.git/` and files in\n`my_project/.idea/` are excluded. Likewise, files ending with `*.pyc` are\nexcluded.\n\nIf you want only know if any changes **would be** made, add `--check-only` option:\n```shell\nwhitespace-format \\\n --exclude ".git/|.idea/|.pyc$" \\\n --check-only \\\n --new-line-marker linux \\\n --normalize-new-line-markers \\\n foo.txt my_project/\n```\nThis command can be used as a validation step before checking the source files\ninto a version control system. The command outputs non-zero exit code if any\nof the files would be formatted.\n\n### Options\n\n* `--check-only` -- Do not format files. Only report which files would be formatted.\n* `--follow-symlinks` -- Follow symbolic links when searching for files.\n* `--exclude=REGEX` -- Regular expression that specifies which files to exclude.\nThe regular expression is evaluated on the path of each file.\n* `--verbose` -- Print more messages than normally.\n* `--quiet` -- Do not print any messages, except for errors when reading or writing files.\n\n### Formatting options\n\n* `--add-new-line-marker-at-end-of-file` -- Add missing new line marker at end of each file.\n* `--remove-new-line-marker-from-end-of-file` -- Remove all new line marker(s) from the end of each file.\nThis option is ignored when `--add-new-line-marker-at-end-of-file` is used.\nEmpty lines at the end of the file are removed.\n* `--normalize-new-line-markers` -- Make new line markers consistent in each file\nby replacing `\\\\r\\\\n`, `\\\\n`, and `\\r` with a consistent new line marker.\n* `--remove-trailing-whitespace` -- Remove whitespace at the end of each line.\n* `--remove-trailing-empty-lines` -- Remove empty lines at the end of each file.\n* `--new-line-marker=MARKER` -- This option specifies what new line marker to use.\n`MARKER` must be one of the following:\n * `auto` -- Use new line marker that is the most common in each individual file.\n If no new line marker is present in the file, Linux `\\n` is used.\n This is the default option.\n * `linux` -- Use Linux new line marker `\\\\n`.\n * `mac` -- Use Mac new line marker `\\\\r`.\n * `windows` -- Use Windows new line marker `\\\\r\\\\n`.\n* `--encoding` -- Text encoding for both reading and writing files. Default encoding is `utf-8`.\nList of supported encodings can be found at\nhttps://docs.python.org/3/library/codecs.html#standard-encodings\n\nNote that input files can contain an arbitrary mix of new line markers `\\n`,\n`\\r`, `\\r\\n` even within the same file. The option `--new-line-marker`\nspecifies the character that should be in the formatted file.\n\nAn opinionated combination of options is:\n```shell\nwhitespace-format \\\n --new-line-marker=linux \\\n --add-new-line-marker-at-end-of-file \\\n --normalize-new-line-markers \\\n --remove-trailing-whitespace \\\n --remove-trailing-empty-lines \\\n foo.txt my_project/\n```\nThis should work well for common programming languages (e.g. Python, Java,\nC/C++, JavaScript) and common text file formats (e.g. CSV, LaTeX, JSON, YAML,\nHTML, MarkDown).\n\n### Empty files\n\nThere are separate options for handling empty files and files consisting of\nwhitespace characters only:\n\n* `--normalize-empty-files=MODE`\n* `--normalize-whitespace-only-files=MODE`\n\nwhere `MODE` is one of the following:\n\n* `ignore` -- Leave the file as is. This is the default option.\n* `empty` -- Replace the file with an empty file.\n* `one-line` -- Replace each file with a file consisting of a single new line marker.\n\nDepending on the mode, an empty file or a whitespace-only file will be either\nignored, replaced by a zero-byte file, or replaced by a file consisting of\nsingle end of line marker.\n\nIf `--normalize-whitespace-only-files` is set to `empty`,\n`--normalize-empty-files setting` set to `empty` as well. In other words,\ncombination `--normalize-whitespace-only-files=empty` and\n`--normalize-empty-files=one-line` is not allowed, since it would lead to\nbehavior that is not idempotent.\n\n### Special characters\n\n* `--replace-tabs-with-spaces=N` -- Replace tabs with spaces.\nWhere is `N` is the number of spaces. If `N` is negative, tabs are not replaced.\nDefault value is `-1`.\n\n* `--normalize-non-standard-whitespace=MODE` -- Replace or remove\nnon-standard whitespace characters (`\\v` and `\\f`). `MODE` must be one of the following:\n * `ignore` -- Leave `\\v` and `f` as is. This is the default option.\n * `replace` -- Replace any occurrence of `\\v` or `\\f` with a single space.\n * `remove` -- Remove all occurrences of `\\v` and `\\f`\n\n## License\n\nMIT\n\n## MacOS development setup\n\n1) Make sure you have [brew](https://brew.sh/) package manager installed.\n\n2) Install [pyenv](https://github.com/pyenv/pyenv), [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv)\n and [poetry](https://python-poetry.org/):\n ```shell\n brew install pyenv\n brew install pyenv-virtualenv\n brew install poetry\n ```\n\n3) Create Python virtual environment with the correct Python version:\n ```shell\n make install-python\n make create-environment\n ```\n\n4) Add the following lines to `.zshrc` or `.bash_profile` and restart the terminal:\n ```shell\n # Pyenv settings\n export PYENV_ROOT="$HOME/.pyenv"\n export PATH="$PYENV_ROOT/bin:$PATH"\n eval "$(pyenv init --path)"\n eval "$(pyenv virtualenv-init -)"\n ```\n\n5) Install all dependencies\n ```shell\n make install-dependecies\n ```\n\nIf you need to delete the Python virtual environment, you can do so with the\ncommand `make delete-environment`.\n\n## Running unit tests and code checks\n\nIf you make code change, run unit tests and code checks with the command:\n```shell\nmake clean whitespace-format-check isort-check black-check flake8 pydocstyle pylint mypy test coverage\n```\n\nEach make target runs different checks:\n- `clean` deletes temporary files\n- `whitespace-format-check` runs [whitespace-format](https://github.com/DavidPal/whitespace-format) checker on all files\n- `isort-check` runs [isort](https://pycqa.github.io/isort/) checker of imports in `*.py` files\n- `black-check` runs [black](https://github.com/psf/black/) code format checker on `*.py` files\n- `flake8` runs [flake8](https://flake8.pycqa.org/) code style checker on `*.py` files\n- `pydocstyle` runs [pydocstyle](http://www.pydocstyle.org/) docstring checker on `*.py` files\n- `pylint` runs [pylint](https://pylint.org/) code checker on `*.py` files\n- `mypy` runs [mypy](http://mypy-lang.org/) type checker on `*.py` files\n- `test` runs unit tests\n- `coverage` generates code coverage report\n\nYou can automatically format code with the command:\n```shell\nmake isort-format black-format whitespace-format\n```\n\n## Modifying dependencies\n\nThe list of Python packages that this project depends on is specified in\n`pyproject.toml` and in `poetry.lock` files. The file `pyproject.toml` can be\nedited by humans. The file `poetry.lock` is automatically generated by\n`poetry`.\n\nInstall a development dependency with the command:\n```shell\npoetry add --dev <some_new_python_tool>\n```\n\nInstall a new production dependency with the command:\n```shell\npoetry add <some_python_library>\n```\n\n### Manual modification of `pyproject.toml`\n\nInstead of using `poetry add` command, you can edit `pyproject.toml` file. Then,\nregenerate `poetry.lock` file with the command:\n```shell\npoetry lock\n```\nor the command:\n```shell\npoetry lock --no-update\n```\nThe latter command does not update already locked packages.\n\n### Fixing broken Python environment\n\nIf your Python virtual environment becomes broken or polluted with unnecessary\npackages, delete it, recreate it from scratch and install dependencies a fresh\nwith the following commands:\n```shell\nmake delete-environment\nmake create-environment\nmake install-dependencies\n```\n',
|
14
|
-
'author': 'David Pal',
|
15
|
-
'author_email': 'davidko.pal@gmail.com',
|
16
|
-
'maintainer': 'None',
|
17
|
-
'maintainer_email': 'None',
|
18
|
-
'url': 'https://github.com/DavidPal/whitespace-format',
|
19
|
-
'py_modules': modules,
|
20
|
-
'entry_points': entry_points,
|
21
|
-
'python_requires': '>=3.7.2,<4.0.0',
|
22
|
-
}
|
23
|
-
|
24
|
-
|
25
|
-
setup(**setup_kwargs)
|
File without changes
|