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.
@@ -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