meson-python-sources-fixer 1.0__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.
@@ -0,0 +1,19 @@
1
+ Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: meson-python-sources-fixer
3
+ Version: 1.0
4
+ Summary: Linter to list all Python sources in meson.build
5
+ Author-email: Omar Sandoval <osandov@osandov.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/osandov/meson-python-sources-fixer
8
+ Project-URL: Issues, https://github.com/osandov/meson-python-sources-fixer/issues
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: regex
13
+ Provides-Extra: test
14
+ Requires-Dist: pyfakefs; extra == "test"
15
+ Dynamic: license-file
16
+
17
+ # meson-python-sources-fixer
18
+
19
+ This is a minimal linter that checks that all Python source files in a Python
20
+ package are listed in a `meson.build` file.
21
+
22
+ ## Installation and Usage
23
+
24
+ To use it via [pre-commit](https://pre-commit.com/), add the following to your
25
+ `.pre-commit-config.yaml`:
26
+
27
+ ```yaml
28
+ repos:
29
+ - repo: https://github.com/osandov/meson-python-sources-fixer
30
+ rev: v1.0
31
+ hooks:
32
+ - id: meson-python-sources-fixer
33
+ ```
34
+
35
+ To run it manually:
36
+
37
+ ```console
38
+ $ pip install meson-python-sources-fixer
39
+ $ cd <project-directory>
40
+ $ meson-python-sources-fixer
41
+ ```
42
+
43
+ By default, `meson-python-sources-fixer` will automatically create or rewrite
44
+ `meson.build` files for Python packages as needed. This behavior can be changed
45
+ with the `--check` and `--diff` options.
46
+
47
+ ## Expected Format
48
+
49
+ `meson-python-sources-fixer` expects a specific layout and format.
50
+
51
+ ### Root `meson.build` File
52
+
53
+ By default, the root `meson.build` file must have two things:
54
+
55
+ 1. A variable initialized to
56
+ [`import('python').find_installation()`](https://mesonbuild.com/Python-module.html#find_installation).
57
+ 2. A [`subdir()`](https://mesonbuild.com/Reference-manual_functions.html#subdir)
58
+ call for each top-level Python package in the project.
59
+
60
+ For example:
61
+
62
+ ```console
63
+ $ tree
64
+ .
65
+ ├── meson.build
66
+ └── pkg
67
+ ├── foo.py
68
+ ├── __init__.py
69
+ ├── meson.build
70
+ └── nested
71
+ ├── bar.py
72
+ └── __init__.py
73
+ $ cat meson.build
74
+ ...
75
+ py = import('python').find_installation()
76
+
77
+ subdir('pkg')
78
+ ```
79
+
80
+ By default, each package is assumed to be installed at the top-level of the
81
+ Python installation. E.g., `subdir('pkg')` and `subdir('src/pkg')` would both
82
+ be installed as `/usr/lib/pythonX.Y/site-packages/pkg`.
83
+
84
+ The `import('python').find_installation()` requirement can be overridden with
85
+ the `--py-installation` option.
86
+
87
+ The list of packages and their installation locations can be overridden with
88
+ the `--package` option.
89
+
90
+ `meson-python-sources-fixer` will not modify the root `meson.build` file.
91
+
92
+ ### Package `meson.build` Files
93
+
94
+ Each top-level Python package should have its own `meson.build` file listing
95
+ its sources files with a call to
96
+ [`py.install_sources()`](https://mesonbuild.com/Python-module.html#install_sources)
97
+ (where `py` is the `python_installation` object defined in the root
98
+ `meson.build` file) per subpackage. The sources should be lexicographically
99
+ sorted, as should the subpackages. For example:
100
+
101
+ ```console
102
+ $ cat pkg/meson.build
103
+ py.install_sources(
104
+ '__init__.py',
105
+ 'foo.py',
106
+ subdir: 'pkg',
107
+ )
108
+
109
+ py.install_sources(
110
+ 'nested/__init__.py',
111
+ 'nested/bar.py',
112
+ subdir: 'pkg/nested',
113
+ )
114
+ ```
115
+
116
+ Note that subpackages (e.g., `pkg/nested`) should not have their own
117
+ `meson.build` files. They are represented in the top-level package.
118
+
119
+ ## Limitations
120
+
121
+ `meson-python-sources-fixer` is relatively simple and has a few limitations:
122
+
123
+ * It requires an `__init__.py` file in each package.
124
+ * It does not require a specific code style, but if a file needs to be updated,
125
+ it will not maintain the existing code style.
126
+ * It allows comments inside of and between calls to `py.install_sources()`, but
127
+ if a file needs to be updated, it cannot preserve comments and will therefore
128
+ refuse to update the file.
129
+ * It does not understand variables, dynamic lists of sources, loops,
130
+ conditionals, etc.
@@ -0,0 +1,114 @@
1
+ # meson-python-sources-fixer
2
+
3
+ This is a minimal linter that checks that all Python source files in a Python
4
+ package are listed in a `meson.build` file.
5
+
6
+ ## Installation and Usage
7
+
8
+ To use it via [pre-commit](https://pre-commit.com/), add the following to your
9
+ `.pre-commit-config.yaml`:
10
+
11
+ ```yaml
12
+ repos:
13
+ - repo: https://github.com/osandov/meson-python-sources-fixer
14
+ rev: v1.0
15
+ hooks:
16
+ - id: meson-python-sources-fixer
17
+ ```
18
+
19
+ To run it manually:
20
+
21
+ ```console
22
+ $ pip install meson-python-sources-fixer
23
+ $ cd <project-directory>
24
+ $ meson-python-sources-fixer
25
+ ```
26
+
27
+ By default, `meson-python-sources-fixer` will automatically create or rewrite
28
+ `meson.build` files for Python packages as needed. This behavior can be changed
29
+ with the `--check` and `--diff` options.
30
+
31
+ ## Expected Format
32
+
33
+ `meson-python-sources-fixer` expects a specific layout and format.
34
+
35
+ ### Root `meson.build` File
36
+
37
+ By default, the root `meson.build` file must have two things:
38
+
39
+ 1. A variable initialized to
40
+ [`import('python').find_installation()`](https://mesonbuild.com/Python-module.html#find_installation).
41
+ 2. A [`subdir()`](https://mesonbuild.com/Reference-manual_functions.html#subdir)
42
+ call for each top-level Python package in the project.
43
+
44
+ For example:
45
+
46
+ ```console
47
+ $ tree
48
+ .
49
+ ├── meson.build
50
+ └── pkg
51
+ ├── foo.py
52
+ ├── __init__.py
53
+ ├── meson.build
54
+ └── nested
55
+ ├── bar.py
56
+ └── __init__.py
57
+ $ cat meson.build
58
+ ...
59
+ py = import('python').find_installation()
60
+
61
+ subdir('pkg')
62
+ ```
63
+
64
+ By default, each package is assumed to be installed at the top-level of the
65
+ Python installation. E.g., `subdir('pkg')` and `subdir('src/pkg')` would both
66
+ be installed as `/usr/lib/pythonX.Y/site-packages/pkg`.
67
+
68
+ The `import('python').find_installation()` requirement can be overridden with
69
+ the `--py-installation` option.
70
+
71
+ The list of packages and their installation locations can be overridden with
72
+ the `--package` option.
73
+
74
+ `meson-python-sources-fixer` will not modify the root `meson.build` file.
75
+
76
+ ### Package `meson.build` Files
77
+
78
+ Each top-level Python package should have its own `meson.build` file listing
79
+ its sources files with a call to
80
+ [`py.install_sources()`](https://mesonbuild.com/Python-module.html#install_sources)
81
+ (where `py` is the `python_installation` object defined in the root
82
+ `meson.build` file) per subpackage. The sources should be lexicographically
83
+ sorted, as should the subpackages. For example:
84
+
85
+ ```console
86
+ $ cat pkg/meson.build
87
+ py.install_sources(
88
+ '__init__.py',
89
+ 'foo.py',
90
+ subdir: 'pkg',
91
+ )
92
+
93
+ py.install_sources(
94
+ 'nested/__init__.py',
95
+ 'nested/bar.py',
96
+ subdir: 'pkg/nested',
97
+ )
98
+ ```
99
+
100
+ Note that subpackages (e.g., `pkg/nested`) should not have their own
101
+ `meson.build` files. They are represented in the top-level package.
102
+
103
+ ## Limitations
104
+
105
+ `meson-python-sources-fixer` is relatively simple and has a few limitations:
106
+
107
+ * It requires an `__init__.py` file in each package.
108
+ * It does not require a specific code style, but if a file needs to be updated,
109
+ it will not maintain the existing code style.
110
+ * It allows comments inside of and between calls to `py.install_sources()`, but
111
+ if a file needs to be updated, it cannot preserve comments and will therefore
112
+ refuse to update the file.
113
+ * It does not understand variables, dynamic lists of sources, loops,
114
+ conditionals, etc.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: meson-python-sources-fixer
3
+ Version: 1.0
4
+ Summary: Linter to list all Python sources in meson.build
5
+ Author-email: Omar Sandoval <osandov@osandov.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/osandov/meson-python-sources-fixer
8
+ Project-URL: Issues, https://github.com/osandov/meson-python-sources-fixer/issues
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: regex
13
+ Provides-Extra: test
14
+ Requires-Dist: pyfakefs; extra == "test"
15
+ Dynamic: license-file
16
+
17
+ # meson-python-sources-fixer
18
+
19
+ This is a minimal linter that checks that all Python source files in a Python
20
+ package are listed in a `meson.build` file.
21
+
22
+ ## Installation and Usage
23
+
24
+ To use it via [pre-commit](https://pre-commit.com/), add the following to your
25
+ `.pre-commit-config.yaml`:
26
+
27
+ ```yaml
28
+ repos:
29
+ - repo: https://github.com/osandov/meson-python-sources-fixer
30
+ rev: v1.0
31
+ hooks:
32
+ - id: meson-python-sources-fixer
33
+ ```
34
+
35
+ To run it manually:
36
+
37
+ ```console
38
+ $ pip install meson-python-sources-fixer
39
+ $ cd <project-directory>
40
+ $ meson-python-sources-fixer
41
+ ```
42
+
43
+ By default, `meson-python-sources-fixer` will automatically create or rewrite
44
+ `meson.build` files for Python packages as needed. This behavior can be changed
45
+ with the `--check` and `--diff` options.
46
+
47
+ ## Expected Format
48
+
49
+ `meson-python-sources-fixer` expects a specific layout and format.
50
+
51
+ ### Root `meson.build` File
52
+
53
+ By default, the root `meson.build` file must have two things:
54
+
55
+ 1. A variable initialized to
56
+ [`import('python').find_installation()`](https://mesonbuild.com/Python-module.html#find_installation).
57
+ 2. A [`subdir()`](https://mesonbuild.com/Reference-manual_functions.html#subdir)
58
+ call for each top-level Python package in the project.
59
+
60
+ For example:
61
+
62
+ ```console
63
+ $ tree
64
+ .
65
+ ├── meson.build
66
+ └── pkg
67
+ ├── foo.py
68
+ ├── __init__.py
69
+ ├── meson.build
70
+ └── nested
71
+ ├── bar.py
72
+ └── __init__.py
73
+ $ cat meson.build
74
+ ...
75
+ py = import('python').find_installation()
76
+
77
+ subdir('pkg')
78
+ ```
79
+
80
+ By default, each package is assumed to be installed at the top-level of the
81
+ Python installation. E.g., `subdir('pkg')` and `subdir('src/pkg')` would both
82
+ be installed as `/usr/lib/pythonX.Y/site-packages/pkg`.
83
+
84
+ The `import('python').find_installation()` requirement can be overridden with
85
+ the `--py-installation` option.
86
+
87
+ The list of packages and their installation locations can be overridden with
88
+ the `--package` option.
89
+
90
+ `meson-python-sources-fixer` will not modify the root `meson.build` file.
91
+
92
+ ### Package `meson.build` Files
93
+
94
+ Each top-level Python package should have its own `meson.build` file listing
95
+ its sources files with a call to
96
+ [`py.install_sources()`](https://mesonbuild.com/Python-module.html#install_sources)
97
+ (where `py` is the `python_installation` object defined in the root
98
+ `meson.build` file) per subpackage. The sources should be lexicographically
99
+ sorted, as should the subpackages. For example:
100
+
101
+ ```console
102
+ $ cat pkg/meson.build
103
+ py.install_sources(
104
+ '__init__.py',
105
+ 'foo.py',
106
+ subdir: 'pkg',
107
+ )
108
+
109
+ py.install_sources(
110
+ 'nested/__init__.py',
111
+ 'nested/bar.py',
112
+ subdir: 'pkg/nested',
113
+ )
114
+ ```
115
+
116
+ Note that subpackages (e.g., `pkg/nested`) should not have their own
117
+ `meson.build` files. They are represented in the top-level package.
118
+
119
+ ## Limitations
120
+
121
+ `meson-python-sources-fixer` is relatively simple and has a few limitations:
122
+
123
+ * It requires an `__init__.py` file in each package.
124
+ * It does not require a specific code style, but if a file needs to be updated,
125
+ it will not maintain the existing code style.
126
+ * It allows comments inside of and between calls to `py.install_sources()`, but
127
+ if a file needs to be updated, it cannot preserve comments and will therefore
128
+ refuse to update the file.
129
+ * It does not understand variables, dynamic lists of sources, loops,
130
+ conditionals, etc.
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ meson_python_sources_fixer.py
4
+ pyproject.toml
5
+ meson_python_sources_fixer.egg-info/PKG-INFO
6
+ meson_python_sources_fixer.egg-info/SOURCES.txt
7
+ meson_python_sources_fixer.egg-info/dependency_links.txt
8
+ meson_python_sources_fixer.egg-info/entry_points.txt
9
+ meson_python_sources_fixer.egg-info/requires.txt
10
+ meson_python_sources_fixer.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ meson-python-sources-fixer = meson_python_sources_fixer:main
@@ -0,0 +1 @@
1
+ meson_python_sources_fixer
@@ -0,0 +1,504 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ import argparse
6
+ import ast
7
+ import difflib
8
+ from pathlib import Path
9
+ import sys
10
+ from typing import Dict, Iterable, Literal, Mapping, Optional, Set
11
+
12
+ import regex
13
+
14
+ if sys.version_info >= (3, 10):
15
+ from itertools import pairwise # novermin
16
+ else:
17
+
18
+ def pairwise(iterable):
19
+ iterator = iter(iterable)
20
+ a = next(iterator, None)
21
+
22
+ for b in iterator:
23
+ yield a, b
24
+ a = b
25
+
26
+
27
+ def print_bold(s: str, *, color: bool) -> None:
28
+ if color:
29
+ sys.stderr.write("\033[1m" + s + "\033[m\n")
30
+ else:
31
+ print(s, file=sys.stderr)
32
+
33
+
34
+ def print_error(s: str, *, color: bool) -> None:
35
+ if color:
36
+ sys.stderr.write("\033[31merror:\033[m " + s + "\n")
37
+ else:
38
+ print(s, file=sys.stderr)
39
+
40
+
41
+ def print_diff(
42
+ old_contents: str, new_contents: str, old_path: str, new_path: str, *, color: bool
43
+ ) -> None:
44
+ diff_lines = difflib.unified_diff(
45
+ old_contents.splitlines(keepends=True),
46
+ new_contents.splitlines(keepends=True),
47
+ old_path,
48
+ new_path,
49
+ )
50
+
51
+ if not color:
52
+ sys.stdout.writelines(diff_lines)
53
+ return
54
+
55
+ control = True
56
+ for line in diff_lines:
57
+ if line.startswith("@"):
58
+ control = False
59
+ code = "36" # Cyan
60
+ elif control:
61
+ code = "1" # Bold
62
+ elif line.startswith("+"):
63
+ code = "32" # Green
64
+ elif line.startswith("-"):
65
+ code = "31" # Red
66
+ else:
67
+ code = ""
68
+
69
+ if code:
70
+ parts = ["\033[", code, "m"]
71
+ if line.endswith("\n"):
72
+ parts.append(line[:-1])
73
+ parts.append("\033[m\n")
74
+ else:
75
+ parts.append(line)
76
+ parts.append("\033[m")
77
+ sys.stdout.write("".join(parts))
78
+ else:
79
+ sys.stdout.write(line)
80
+
81
+
82
+ STRING_PATTERN = r"'(?:\\.|[^'])*'"
83
+ COMMENT_PATTERN = r"[#].*"
84
+ NON_NEWLINE_WHITESPACE = r"[^\S\n]*"
85
+ WHITESPACE_OR_COMMENT_CAPTURE = r"(?:\s|(?&comment))*"
86
+ WHITESPACE_OR_COMMENT_EOL_CAPTURE = r"[^\S\n]*(?&comment)?$"
87
+ WHITESPACE_OR_COMMENT = WHITESPACE_OR_COMMENT_CAPTURE.replace(
88
+ "(?&comment)", COMMENT_PATTERN
89
+ )
90
+ WHITESPACE_OR_COMMENT_EOL = WHITESPACE_OR_COMMENT_EOL_CAPTURE.replace(
91
+ "(?&comment)", "(?:" + COMMENT_PATTERN + ")"
92
+ )
93
+
94
+
95
+ PY_INSTALLATION_PATTERN = rf"""
96
+ ^
97
+ {NON_NEWLINE_WHITESPACE}
98
+ (\w+)
99
+ {NON_NEWLINE_WHITESPACE}
100
+ =
101
+ {NON_NEWLINE_WHITESPACE}
102
+ import
103
+ {NON_NEWLINE_WHITESPACE}
104
+ \(
105
+ {WHITESPACE_OR_COMMENT}
106
+ 'python'
107
+ {WHITESPACE_OR_COMMENT}
108
+ \)
109
+ {NON_NEWLINE_WHITESPACE}
110
+ \.
111
+ {NON_NEWLINE_WHITESPACE}
112
+ find_installation
113
+ {NON_NEWLINE_WHITESPACE}
114
+ \(
115
+ """
116
+
117
+
118
+ def find_py_installation(contents: str) -> Optional[str]:
119
+ match = regex.search(
120
+ PY_INSTALLATION_PATTERN, contents, flags=regex.MULTILINE | regex.VERBOSE
121
+ )
122
+ return match.group(1) if match else None
123
+ if not match:
124
+ raise LookupError("import('python').find_installation() not found")
125
+ return match.group(1)
126
+
127
+
128
+ SUBDIR_PATTERN = rf"""
129
+ ^
130
+ {NON_NEWLINE_WHITESPACE}
131
+ subdir
132
+ {NON_NEWLINE_WHITESPACE}
133
+ \(
134
+ {WHITESPACE_OR_COMMENT}
135
+ ({STRING_PATTERN})
136
+ {WHITESPACE_OR_COMMENT}
137
+ \)
138
+ {WHITESPACE_OR_COMMENT_EOL}
139
+ """
140
+
141
+
142
+ def find_subdirs(contents: str) -> Iterable[str]:
143
+ return [
144
+ ast.literal_eval(s)
145
+ for s in regex.findall(
146
+ SUBDIR_PATTERN, contents, flags=regex.MULTILINE | regex.VERBOSE
147
+ )
148
+ ]
149
+
150
+
151
+ def is_python_source(name: str) -> bool:
152
+ return name.endswith(".py") or name.endswith(".pyi") or name == "py.typed"
153
+
154
+
155
+ def find_package_sources(source_path: Path, install_path: Path) -> Dict[str, Set[str]]:
156
+ sources = {}
157
+ if (source_path / "__init__.py").exists():
158
+ for root, dirs, files in source_path.walk():
159
+ dirs[:] = [dir for dir in dirs if (root / dir / "__init__.py").exists()]
160
+
161
+ relative = root.relative_to(source_path)
162
+ sources[str(install_path / relative)] = {
163
+ str(relative / file) for file in files if is_python_source(file)
164
+ }
165
+ return sources
166
+
167
+
168
+ INSTALL_SOURCES_PATTERN = rf"""
169
+ (?(DEFINE)
170
+ (?<posarg>{STRING_PATTERN})
171
+ (?<list>
172
+ \[
173
+ {WHITESPACE_OR_COMMENT_CAPTURE}
174
+ (?:
175
+ (?&posarg)
176
+ {WHITESPACE_OR_COMMENT_CAPTURE}
177
+ ,
178
+ {WHITESPACE_OR_COMMENT_CAPTURE}
179
+ )*
180
+ (?:
181
+ (?&posarg)
182
+ {WHITESPACE_OR_COMMENT_CAPTURE}
183
+ )?
184
+ \]
185
+ )
186
+ (?<posarg_or_list>(?&posarg)|(?&list))
187
+ (?<kwarg>
188
+ (?<keyword>\w+)
189
+ {WHITESPACE_OR_COMMENT_CAPTURE}
190
+ :
191
+ {WHITESPACE_OR_COMMENT_CAPTURE}
192
+ (?<value>{STRING_PATTERN}|\w+)
193
+ )
194
+ (?<comment>{COMMENT_PATTERN})
195
+ )
196
+ ^
197
+ {NON_NEWLINE_WHITESPACE}
198
+ {{}}
199
+ {NON_NEWLINE_WHITESPACE}
200
+ \.
201
+ {NON_NEWLINE_WHITESPACE}
202
+ install_sources
203
+ {NON_NEWLINE_WHITESPACE}
204
+ \(
205
+ (?:
206
+ {WHITESPACE_OR_COMMENT_CAPTURE}
207
+ (?:
208
+ (?&posarg_or_list)
209
+ {WHITESPACE_OR_COMMENT_CAPTURE}
210
+ ,
211
+ {WHITESPACE_OR_COMMENT_CAPTURE}
212
+ )*
213
+ (?:
214
+ (?:
215
+ (?&posarg_or_list)
216
+ {WHITESPACE_OR_COMMENT_CAPTURE}
217
+ )?
218
+ |
219
+ (?:
220
+ (?&kwarg)
221
+ {WHITESPACE_OR_COMMENT_CAPTURE}
222
+ ,
223
+ {WHITESPACE_OR_COMMENT_CAPTURE}
224
+ )*
225
+ (?:
226
+ (?&kwarg)
227
+ {WHITESPACE_OR_COMMENT_CAPTURE}
228
+ )?
229
+ )
230
+ \)
231
+ {WHITESPACE_OR_COMMENT_EOL_CAPTURE}
232
+ |
233
+ (?<unrecognized>.|$)
234
+ )
235
+ """
236
+
237
+
238
+ class CannotFixError(Exception):
239
+ pass
240
+
241
+
242
+ def fix_package_meson_build(
243
+ contents: str,
244
+ py_installation: str,
245
+ package_sources: Mapping[str, Set[str]],
246
+ ) -> str:
247
+ matches = {}
248
+ found_install_sources = {}
249
+ for match in regex.finditer(
250
+ INSTALL_SOURCES_PATTERN.format(regex.escape(py_installation)),
251
+ contents,
252
+ flags=regex.MULTILINE | regex.VERBOSE,
253
+ ):
254
+ if match.group("unrecognized"):
255
+ raise CannotFixError(
256
+ f"unrecognized {py_installation}.install_sources() syntax"
257
+ )
258
+
259
+ kwargs = dict(zip(match.captures("keyword"), match.captures("value")))
260
+ try:
261
+ subdir_arg = kwargs["subdir"]
262
+ except KeyError:
263
+ subdir = "."
264
+ else:
265
+ if not subdir_arg.startswith("'"):
266
+ raise CannotFixError(
267
+ f"unrecognized {py_installation}.install_sources() subdir syntax"
268
+ )
269
+ subdir = ast.literal_eval(subdir_arg)
270
+
271
+ if subdir in matches:
272
+ raise CannotFixError(
273
+ f"duplicate {py_installation}.install_sources(..., subdir: {subdir!r})"
274
+ )
275
+
276
+ matches[subdir] = match
277
+ found_install_sources[subdir] = (
278
+ [ast.literal_eval(arg) for arg in match.captures("posarg")],
279
+ kwargs,
280
+ )
281
+
282
+ expected_install_sources = []
283
+ for subdir, sources in package_sources.items():
284
+ try:
285
+ found_sources, kwargs = found_install_sources[subdir]
286
+ except KeyError:
287
+ kwargs = {"subdir": repr(subdir)}
288
+ else:
289
+ sources = sources | {
290
+ source for source in found_sources if not is_python_source(source)
291
+ }
292
+ expected_install_sources.append((subdir, (sorted(sources), kwargs)))
293
+ expected_install_sources.sort()
294
+
295
+ if list(found_install_sources.items()) == expected_install_sources:
296
+ return contents
297
+
298
+ for match1, match2 in pairwise(matches.values()):
299
+ if regex.search(r"\S", contents, pos=match1.end(), endpos=match2.start()):
300
+ raise CannotFixError(
301
+ f"non-whitespace between {py_installation}.install_sources() calls"
302
+ )
303
+
304
+ if matches:
305
+ matches_start = next(iter(matches.values())).start()
306
+ matches_end = next(reversed(matches.values())).end()
307
+
308
+ if not expected_install_sources:
309
+ if matches_end < len(contents):
310
+ assert contents[matches_end] == "\n"
311
+ matches_end += 1
312
+
313
+ blank_before = matches_start < 2 or contents[matches_start - 2] == "\n"
314
+ blank_after = matches_end == len(contents) or contents[matches_end] == "\n"
315
+ if blank_before and blank_after:
316
+ if matches_end < len(contents):
317
+ matches_end += 1
318
+ elif matches_start > 0:
319
+ matches_start -= 1
320
+
321
+ parts = [contents[:matches_start]]
322
+ else:
323
+ parts = [contents]
324
+ if contents:
325
+ if not contents.endswith("\n"):
326
+ parts.append("\n\n")
327
+ elif not contents.endswith("\n\n"):
328
+ parts.append("\n")
329
+
330
+ for i, (subdir, (expected_sources, kwargs)) in enumerate(expected_install_sources):
331
+ if found_install_sources.get(subdir) == (expected_sources, kwargs):
332
+ parts.append(matches[subdir].group(0))
333
+ else:
334
+ if subdir in matches and matches[subdir].captures("comment"):
335
+ raise CannotFixError(
336
+ f"{py_installation}.install_sources() call contains comments"
337
+ )
338
+ parts.append(py_installation)
339
+ parts.append(".install_sources(\n")
340
+ for source in expected_sources:
341
+ parts.append(f" {source!r},\n")
342
+ for keyword, value in kwargs.items():
343
+ parts.append(f" {keyword}: {value},\n")
344
+ parts.append(")")
345
+
346
+ if i < len(expected_install_sources) - 1:
347
+ parts.append("\n\n")
348
+
349
+ if matches:
350
+ parts.append(contents[matches_end:])
351
+ else:
352
+ parts.append("\n")
353
+
354
+ return "".join(parts)
355
+
356
+
357
+ def update_package_meson_build(
358
+ meson_build_path: Path,
359
+ py_installation: str,
360
+ package_sources: Mapping[str, Set[str]],
361
+ *,
362
+ mode: Literal["update", "check", "diff"],
363
+ color_stdout: bool = False,
364
+ color_stderr: bool = False,
365
+ ) -> bool:
366
+ try:
367
+ contents = meson_build_path.read_text()
368
+ exists = True
369
+ except FileNotFoundError:
370
+ contents = ""
371
+ exists = False
372
+
373
+ orig_contents = contents
374
+
375
+ try:
376
+ contents = fix_package_meson_build(contents, py_installation, package_sources)
377
+ except CannotFixError as e:
378
+ print_error(f"cannot fix {meson_build_path}: {e}", color=color_stderr)
379
+ return False
380
+
381
+ if contents != orig_contents:
382
+ if mode == "update":
383
+ meson_build_path.write_text(contents)
384
+ if exists:
385
+ print_bold(f"updated {meson_build_path}", color=color_stderr)
386
+ else:
387
+ print_bold(f"created {meson_build_path}", color=color_stderr)
388
+ else:
389
+ if mode == "diff":
390
+ print_diff(
391
+ orig_contents,
392
+ contents,
393
+ str(meson_build_path) if exists else "/dev/null",
394
+ str(meson_build_path),
395
+ color=color_stdout,
396
+ )
397
+ sys.stdout.flush()
398
+ if exists:
399
+ print_bold(f"{meson_build_path} is out of date", color=color_stderr)
400
+ else:
401
+ print_bold(f"{meson_build_path} does not exist", color=color_stderr)
402
+ return False
403
+
404
+ return True
405
+
406
+
407
+ def main() -> int:
408
+ parser = argparse.ArgumentParser(
409
+ description="rewrite meson.build files in the current project to include all Python package sources"
410
+ )
411
+ parser.add_argument(
412
+ "--py-installation",
413
+ metavar="NAME",
414
+ help="name of python_installation object (default: find in meson.build)",
415
+ )
416
+ parser.add_argument(
417
+ "--package",
418
+ nargs=2,
419
+ metavar=("SOURCE_PATH", "INSTALL_PATH"),
420
+ type=Path,
421
+ action="append",
422
+ help="paths of packages and where they should be installed (default: find in meson.build)",
423
+ )
424
+ parser.add_argument(
425
+ "--check",
426
+ dest="mode",
427
+ action="store_const",
428
+ const="check",
429
+ default="update",
430
+ help="don't rewrite any files, just exit with status 1 if any files need to be updated",
431
+ )
432
+ parser.add_argument(
433
+ "--diff",
434
+ dest="mode",
435
+ action="store_const",
436
+ const="diff",
437
+ help="don't rewrite any files, just exit with status 1 and print a diff to standard output if any files need to be updated",
438
+ )
439
+ parser.add_argument(
440
+ "--color",
441
+ choices=("auto", "always", "never"),
442
+ default="auto",
443
+ help="colorize output",
444
+ )
445
+ args = parser.parse_args()
446
+
447
+ if args.color == "auto":
448
+ color_stdout = sys.stdout.isatty()
449
+ color_stderr = sys.stderr.isatty()
450
+ else:
451
+ color_stdout = color_stderr = args.color == "always"
452
+
453
+ meson_build_path = Path("meson.build")
454
+ if args.py_installation is None or not args.package:
455
+ meson_build_contents = meson_build_path.read_text()
456
+
457
+ if args.py_installation is None:
458
+ args.py_installation = find_py_installation(meson_build_contents)
459
+ if args.py_installation is None:
460
+ print_error(
461
+ f"{meson_build_path}: import('python').find_installation() not found. Try --py-installation.",
462
+ color=color_stderr,
463
+ )
464
+ return 1
465
+
466
+ if args.package:
467
+ packages = []
468
+ for source_path, install_path in args.package:
469
+ package_sources = find_package_sources(source_path, install_path)
470
+ if not package_sources:
471
+ print_error(f"{source_path}: not a Python package", color=color_stderr)
472
+ return 1
473
+ packages.append((source_path, package_sources))
474
+ else:
475
+ packages = []
476
+ for subdir in find_subdirs(meson_build_contents):
477
+ subdir_path = Path(subdir)
478
+ source_path = subdir_path
479
+ package_sources = find_package_sources(source_path, Path(subdir_path.name))
480
+ if package_sources:
481
+ packages.append((source_path, package_sources))
482
+ if not packages:
483
+ print_error(
484
+ f"{meson_build_path}: no Python package subdir() calls found. Try --package.",
485
+ color=color_stderr,
486
+ )
487
+ return 1
488
+
489
+ exit_status = 0
490
+ for source_path, package_sources in packages:
491
+ if not update_package_meson_build(
492
+ source_path / "meson.build",
493
+ args.py_installation,
494
+ package_sources,
495
+ mode=args.mode,
496
+ color_stdout=color_stdout,
497
+ color_stderr=color_stderr,
498
+ ):
499
+ exit_status = 1
500
+ return exit_status
501
+
502
+
503
+ if __name__ == "__main__":
504
+ sys.exit(main())
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "meson-python-sources-fixer"
3
+ version = "1.0"
4
+ description = "Linter to list all Python sources in meson.build"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ {name = "Omar Sandoval", email = "osandov@osandov.com"}
10
+ ]
11
+ dependencies = ["regex"]
12
+ requires-python = ">= 3.8"
13
+
14
+ [project.urls]
15
+ Homepage = "https://github.com/osandov/meson-python-sources-fixer"
16
+ Issues = "https://github.com/osandov/meson-python-sources-fixer/issues"
17
+
18
+ [project.optional-dependencies]
19
+ test = ["pyfakefs"]
20
+
21
+ [project.scripts]
22
+ meson-python-sources-fixer = "meson_python_sources_fixer:main"
23
+
24
+ [build-system]
25
+ requires = ["setuptools >= 77.0.3"]
26
+ build-backend = "setuptools.build_meta"
27
+
28
+ [tool.isort]
29
+ profile = "black"
30
+ combine_as_imports = true
31
+ force_sort_within_sections = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+