prettycsharp 1.0.0__py3-none-any.whl
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.
- prettycsharp/__init__.py +9 -0
- prettycsharp/__main__.py +5 -0
- prettycsharp/cli.py +250 -0
- prettycsharp/config.py +263 -0
- prettycsharp/doc.py +537 -0
- prettycsharp/formatter.py +106 -0
- prettycsharp/ignore.py +184 -0
- prettycsharp/parsing.py +53 -0
- prettycsharp/printer/__init__.py +28 -0
- prettycsharp/printer/core.py +540 -0
- prettycsharp/printer/declarations.py +623 -0
- prettycsharp/printer/expressions.py +1107 -0
- prettycsharp/printer/statements.py +603 -0
- prettycsharp/project.py +214 -0
- prettycsharp/py.typed +0 -0
- prettycsharp/rewrites.py +732 -0
- prettycsharp/settings.py +192 -0
- prettycsharp/syntax.py +367 -0
- prettycsharp/verify.py +312 -0
- prettycsharp-1.0.0.dist-info/METADATA +212 -0
- prettycsharp-1.0.0.dist-info/RECORD +24 -0
- prettycsharp-1.0.0.dist-info/WHEEL +4 -0
- prettycsharp-1.0.0.dist-info/entry_points.txt +2 -0
- prettycsharp-1.0.0.dist-info/licenses/LICENSE +21 -0
prettycsharp/project.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""What MSBuild project files say: the C# version and the files compiled.
|
|
2
|
+
|
|
3
|
+
Project files are untrusted input, so they are parsed with defusedxml,
|
|
4
|
+
which refuses entity expansion and external references.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from functools import cache
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from defusedxml import ElementTree
|
|
15
|
+
|
|
16
|
+
from prettycsharp.ignore import glob_regex
|
|
17
|
+
|
|
18
|
+
# The parsed XML element. Only defusedxml parses here, and it has no type
|
|
19
|
+
# information, so elements are typed loosely rather than imported from xml.
|
|
20
|
+
Element = Any
|
|
21
|
+
|
|
22
|
+
LATEST = 99.0
|
|
23
|
+
|
|
24
|
+
# The default C# version of each target framework.
|
|
25
|
+
_FRAMEWORK_VERSIONS = (
|
|
26
|
+
(r"^netcoreapp3\.", 8.0),
|
|
27
|
+
(r"^netcoreapp[12]\.", 7.3),
|
|
28
|
+
(r"^netstandard2\.1", 8.0),
|
|
29
|
+
(r"^netstandard", 7.3),
|
|
30
|
+
(r"^net[1-4]\d*$", 7.3),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def lang_version_for(source: Path) -> float | None:
|
|
35
|
+
"""The C# version of the project holding ``source``, or None if it has none."""
|
|
36
|
+
project = find_project(source.resolve().parent)
|
|
37
|
+
if project is None:
|
|
38
|
+
return None
|
|
39
|
+
return project_lang_version(project)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@cache
|
|
43
|
+
def find_project(directory: Path) -> Path | None:
|
|
44
|
+
for folder in (directory, *directory.parents):
|
|
45
|
+
try:
|
|
46
|
+
projects = sorted(folder.glob("*.csproj"))
|
|
47
|
+
except OSError:
|
|
48
|
+
return None
|
|
49
|
+
if projects:
|
|
50
|
+
return projects[0]
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@cache
|
|
55
|
+
def project_lang_version(project: Path) -> float:
|
|
56
|
+
properties = _properties(project)
|
|
57
|
+
version = properties.get("langversion")
|
|
58
|
+
if version:
|
|
59
|
+
parsed = parse_lang_version(version)
|
|
60
|
+
if parsed is not None:
|
|
61
|
+
return parsed
|
|
62
|
+
frameworks = properties.get("targetframeworks") or properties.get("targetframework") or ""
|
|
63
|
+
versions = [framework_lang_version(f.strip()) for f in frameworks.split(";") if f.strip()]
|
|
64
|
+
known = [v for v in versions if v is not None]
|
|
65
|
+
return min(known) if known else LATEST
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_lang_version(text: str) -> float | None:
|
|
69
|
+
value = text.strip().lower()
|
|
70
|
+
if value in ("latest", "latestmajor", "preview", "default", ""):
|
|
71
|
+
return LATEST
|
|
72
|
+
if value.startswith("iso-"):
|
|
73
|
+
return 2.0
|
|
74
|
+
try:
|
|
75
|
+
return float(value)
|
|
76
|
+
except ValueError:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def framework_lang_version(framework: str) -> float | None:
|
|
81
|
+
"""The default C# version of a target framework moniker."""
|
|
82
|
+
name = framework.lower()
|
|
83
|
+
modern = re.match(r"^net(\d+)\.\d+", name)
|
|
84
|
+
if modern:
|
|
85
|
+
major = int(modern.group(1))
|
|
86
|
+
# net5.0 is C# 9, net6.0 C# 10, and each release since adds one.
|
|
87
|
+
return 7.3 if major < 5 else 9.0 + (major - 5)
|
|
88
|
+
for prefix, version in _FRAMEWORK_VERSIONS:
|
|
89
|
+
if re.match(prefix, name):
|
|
90
|
+
return version
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _properties(project: Path) -> dict[str, str]:
|
|
95
|
+
"""LangVersion and target frameworks, from Directory.Build.props and the project."""
|
|
96
|
+
values: dict[str, str] = {}
|
|
97
|
+
props = _find_upward(project.parent, "Directory.Build.props")
|
|
98
|
+
for path in ([props] if props is not None else []) + [project]:
|
|
99
|
+
root = _parse(path)
|
|
100
|
+
if root is None:
|
|
101
|
+
continue
|
|
102
|
+
for element in root.iter():
|
|
103
|
+
tag = _local(element.tag).lower()
|
|
104
|
+
if tag in ("langversion", "targetframework", "targetframeworks") and element.text:
|
|
105
|
+
values[tag] = element.text.strip()
|
|
106
|
+
return values
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _find_upward(directory: Path, name: str) -> Path | None:
|
|
110
|
+
for folder in (directory, *directory.parents):
|
|
111
|
+
candidate = folder / name
|
|
112
|
+
if candidate.is_file():
|
|
113
|
+
return candidate
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _parse(path: Path) -> Element | None:
|
|
118
|
+
try:
|
|
119
|
+
root: Element = ElementTree.parse(str(path)).getroot()
|
|
120
|
+
return root
|
|
121
|
+
except Exception: # noqa: BLE001 - an unreadable project only loses its settings
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _local(tag: object) -> str:
|
|
126
|
+
text = str(tag)
|
|
127
|
+
return text.split("}", 1)[1] if text.startswith("{") else text
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Compiled files ------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def compiled_files(project: Path) -> list[Path]:
|
|
134
|
+
"""The .cs files a project compiles: MSBuild's default glob, then Include and Remove."""
|
|
135
|
+
root = _parse(project)
|
|
136
|
+
folder = project.parent
|
|
137
|
+
files: list[Path] = []
|
|
138
|
+
default = True
|
|
139
|
+
includes: list[str] = []
|
|
140
|
+
removes: list[str] = []
|
|
141
|
+
if root is not None:
|
|
142
|
+
for element in root.iter():
|
|
143
|
+
tag = _local(element.tag).lower()
|
|
144
|
+
if tag == "enabledefaultcompileitems" and (element.text or "").strip().lower() == "false":
|
|
145
|
+
default = False
|
|
146
|
+
elif tag == "compile":
|
|
147
|
+
if element.get("Include"):
|
|
148
|
+
includes.extend(_split_items(element.get("Include") or ""))
|
|
149
|
+
if element.get("Remove"):
|
|
150
|
+
removes.extend(_split_items(element.get("Remove") or ""))
|
|
151
|
+
if default:
|
|
152
|
+
for path in sorted(folder.rglob("*.cs")):
|
|
153
|
+
parts = path.relative_to(folder).parts[:-1]
|
|
154
|
+
if not any(p.lower() in ("bin", "obj") for p in parts):
|
|
155
|
+
files.append(path)
|
|
156
|
+
for pattern in includes:
|
|
157
|
+
files.extend(_glob(folder, pattern))
|
|
158
|
+
removed = [re.compile(glob_regex(_normalize(p))) for p in removes]
|
|
159
|
+
result: list[Path] = []
|
|
160
|
+
seen: set[Path] = set()
|
|
161
|
+
for path in files:
|
|
162
|
+
resolved = path.resolve()
|
|
163
|
+
if resolved in seen:
|
|
164
|
+
continue
|
|
165
|
+
try:
|
|
166
|
+
relative = path.resolve().relative_to(folder.resolve()).as_posix()
|
|
167
|
+
except ValueError:
|
|
168
|
+
relative = None
|
|
169
|
+
if relative is not None and any(r.fullmatch(relative) for r in removed):
|
|
170
|
+
continue
|
|
171
|
+
seen.add(resolved)
|
|
172
|
+
result.append(path)
|
|
173
|
+
return result
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _split_items(text: str) -> list[str]:
|
|
177
|
+
return [item.strip() for item in text.split(";") if item.strip() and "$(" not in item]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _normalize(pattern: str) -> str:
|
|
181
|
+
return pattern.replace("\\", "/")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _glob(folder: Path, pattern: str) -> list[Path]:
|
|
185
|
+
normalized = _normalize(pattern)
|
|
186
|
+
if any(c in normalized for c in "*?"):
|
|
187
|
+
return sorted(p for p in folder.glob(normalized) if p.suffix.lower() == ".cs")
|
|
188
|
+
path = folder / normalized
|
|
189
|
+
return [path] if path.is_file() else []
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def solution_projects(solution: Path) -> list[Path]:
|
|
193
|
+
"""The C# projects a .sln or .slnx lists."""
|
|
194
|
+
projects: list[Path] = []
|
|
195
|
+
if solution.suffix.lower() == ".slnx":
|
|
196
|
+
root = _parse(solution)
|
|
197
|
+
if root is not None:
|
|
198
|
+
for element in root.iter():
|
|
199
|
+
path = element.get("Path")
|
|
200
|
+
if _local(element.tag).lower() == "project" and path and path.lower().endswith(".csproj"):
|
|
201
|
+
projects.append(solution.parent / _normalize(path))
|
|
202
|
+
return projects
|
|
203
|
+
try:
|
|
204
|
+
text = solution.read_text(encoding="utf-8-sig", errors="replace")
|
|
205
|
+
except OSError:
|
|
206
|
+
return projects
|
|
207
|
+
for match in re.finditer(r'^Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"', text, re.M | re.I):
|
|
208
|
+
projects.append(solution.parent / _normalize(match.group(1)))
|
|
209
|
+
return projects
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def clear_caches() -> None:
|
|
213
|
+
find_project.cache_clear()
|
|
214
|
+
project_lang_version.cache_clear()
|
prettycsharp/py.typed
ADDED
|
File without changes
|