fluidattacks_core_filesystem 12.0.0__tar.gz → 14.0.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.
Files changed (29) hide show
  1. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/.gitignore +4 -0
  2. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/PKG-INFO +1 -1
  3. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/fluidattacks_core/filesystem/defaults.py +16 -0
  4. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/fluidattacks_core/filesystem/detection.py +22 -2
  5. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/fluidattacks_core/filesystem/partitioning.py +63 -5
  6. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/import-linter.cfg +1 -10
  7. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/pyproject.toml +1 -1
  8. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/tests/test_partitioning.py +134 -2
  9. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/tests/test_projects.py +10 -0
  10. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/uv.lock +1 -1
  11. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/.envrc +0 -0
  12. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/build/default.nix +0 -0
  13. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/build/filter.nix +0 -0
  14. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/flake.lock +0 -0
  15. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/flake.nix +0 -0
  16. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/fluidattacks_core/filesystem/__init__.py +0 -0
  17. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/fluidattacks_core/filesystem/language.py +0 -0
  18. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/fluidattacks_core/filesystem/parts.py +0 -0
  19. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/fluidattacks_core/filesystem/projects.py +0 -0
  20. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/fluidattacks_core/filesystem/py.typed +0 -0
  21. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/mypy.ini +0 -0
  22. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/ruff.toml +0 -0
  23. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/tests/__init__.py +0 -0
  24. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/tests/data/partitioning_snapshot.golden.json +0 -0
  25. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/tests/test_defaults.py +0 -0
  26. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/tests/test_extract_db.py +0 -0
  27. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/tests/test_language_detection.py +0 -0
  28. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/tests/test_partitioning_snapshot.py +0 -0
  29. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-14.0.0}/tests/test_parts.py +0 -0
@@ -1,5 +1,9 @@
1
+ dist/
2
+ dist-*/
3
+ /.cargo-cache
1
4
  target/
2
5
  .direnv/
3
6
  .venv/
4
7
  .vscode/settings.json
5
8
  .zed/settings.json
9
+ *.so
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fluidattacks_core_filesystem
3
- Version: 12.0.0
3
+ Version: 14.0.0
4
4
  Summary: Fluid Attacks Core Filesystem Library
5
5
  Author-email: Development <development@fluidattacks.com>
6
6
  License: MPL-2.0
@@ -23,6 +23,22 @@ class MarkerSet(TypedDict):
23
23
  globs: list[str]
24
24
 
25
25
 
26
+ class WorkspaceSpec(TypedDict):
27
+ declares: frozenset[str]
28
+ claims: frozenset[str]
29
+
30
+
31
+ BUNDLE_DIR_GLOBS = ["*.xcodeproj", "*.xcworkspace"]
32
+
33
+
34
+ WORKSPACE_MARKERS: dict[str, WorkspaceSpec] = {
35
+ "gradle": {
36
+ "declares": frozenset({"settings.gradle", "settings.gradle.kts"}),
37
+ "claims": frozenset({"build.gradle", "build.gradle.kts"}),
38
+ },
39
+ }
40
+
41
+
26
42
  CONFIG_MARKERS: dict[Language, MarkerSet] = {
27
43
  Language.Java: {
28
44
  "names": {
@@ -3,11 +3,11 @@ from __future__ import annotations
3
3
  from fnmatch import fnmatch
4
4
  from typing import TYPE_CHECKING
5
5
 
6
- from .defaults import CONFIG_MARKERS, MANIFEST_MARKERS, MarkerSet
6
+ from .defaults import CONFIG_MARKERS, MANIFEST_MARKERS, WORKSPACE_MARKERS, MarkerSet
7
7
  from .language import Language
8
8
 
9
9
  if TYPE_CHECKING:
10
- from collections.abc import Iterable
10
+ from collections.abc import Collection, Iterable
11
11
 
12
12
  __all__ = ["detect_anchor_language", "detect_languages_in_dir", "is_config_marker"]
13
13
 
@@ -17,6 +17,14 @@ _FOLDED_MARKER_NAMES = frozenset(
17
17
 
18
18
  _MARKER_GLOBS = tuple(pat for markers in CONFIG_MARKERS.values() for pat in markers["globs"])
19
19
 
20
+ _FOLDED_WORKSPACES = {
21
+ kind: (
22
+ frozenset(name.casefold() for name in spec["declares"]),
23
+ frozenset(name.casefold() for name in spec["claims"]),
24
+ )
25
+ for kind, spec in WORKSPACE_MARKERS.items()
26
+ }
27
+
20
28
  _FOLDED_MANIFESTS = {
21
29
  language: frozenset(name.casefold() for name in names)
22
30
  for language, names in MANIFEST_MARKERS.items()
@@ -112,3 +120,15 @@ def detect_anchor_language(files: Iterable[str]) -> Language | None:
112
120
  sorted(anchors, key=lambda language: language.value)
113
121
  )
114
122
  return ranked[0] if len(ranked) == 1 else Language.Unknown
123
+
124
+
125
+ def declared_workspaces(files: Iterable[str]) -> frozenset[str]:
126
+ folded = {name.casefold() for name in files}
127
+ return frozenset(
128
+ kind for kind, (declares, _) in _FOLDED_WORKSPACES.items() if declares & folded
129
+ )
130
+
131
+
132
+ def is_claimed_manifest(name: str, kinds: Collection[str]) -> bool:
133
+ folded = name.casefold()
134
+ return any(folded in _FOLDED_WORKSPACES[kind][1] for kind in kinds)
@@ -9,8 +9,14 @@ from fnmatch import fnmatch
9
9
  from pathlib import Path
10
10
  from typing import TYPE_CHECKING
11
11
 
12
- from .defaults import EXTENDED_EXCLUDE_DIRS
13
- from .detection import detect_anchor_language, detect_languages_in_dir, is_config_marker
12
+ from .defaults import BUNDLE_DIR_GLOBS, EXTENDED_EXCLUDE_DIRS
13
+ from .detection import (
14
+ declared_workspaces,
15
+ detect_anchor_language,
16
+ detect_languages_in_dir,
17
+ is_claimed_manifest,
18
+ is_config_marker,
19
+ )
14
20
  from .parts import Part, PartIdentity, PartTree
15
21
 
16
22
  if TYPE_CHECKING:
@@ -63,11 +69,63 @@ def _root_filenames(listing: DirListing, root_path: Path) -> tuple[str, ...]:
63
69
  return ()
64
70
 
65
71
 
66
- def _discover_potential_parts(listing: DirListing) -> tuple[PotentialPart, ...]:
72
+ def _workspace_kinds_by_dir(listing: DirListing) -> Mapping[Path, frozenset[str]]:
73
+ return {
74
+ dir_path: kinds
75
+ for dir_path, filenames in listing
76
+ if (kinds := declared_workspaces(filenames))
77
+ }
78
+
79
+
80
+ def _claiming_kinds(dir_path: Path, workspaces: Mapping[Path, frozenset[str]]) -> frozenset[str]:
81
+ kinds: set[str] = set()
82
+ current = dir_path
83
+ while current != current.parent:
84
+ current = current.parent
85
+ kinds.update(workspaces.get(current, frozenset()))
86
+ return frozenset(kinds)
87
+
88
+
89
+ def _is_bundle_name(name: str) -> bool:
90
+ return any(fnmatch(name, pattern) for pattern in BUNDLE_DIR_GLOBS)
91
+
92
+
93
+ def _within_bundle(dir_path: Path, root_path: Path) -> bool:
94
+ current = dir_path
95
+ while current != root_path:
96
+ if _is_bundle_name(current.name):
97
+ return True
98
+ if current == current.parent:
99
+ return False
100
+ current = current.parent
101
+ return False
102
+
103
+
104
+ def _anchor_names(
105
+ dir_path: Path,
106
+ filenames: Sequence[str],
107
+ workspaces: Mapping[Path, frozenset[str]],
108
+ root_path: Path,
109
+ ) -> tuple[str, ...]:
110
+ if _within_bundle(dir_path, root_path):
111
+ return ()
112
+ kinds = _claiming_kinds(dir_path, workspaces)
113
+ if not kinds:
114
+ return tuple(filenames)
115
+ return tuple(name for name in filenames if not is_claimed_manifest(name, kinds))
116
+
117
+
118
+ def _discover_potential_parts(listing: DirListing, root_path: Path) -> tuple[PotentialPart, ...]:
119
+ workspaces = _workspace_kinds_by_dir(listing)
67
120
  return tuple(
68
121
  (dir_path, _find_config_files(dir_path, filenames), language.value)
69
122
  for dir_path, filenames in listing
70
- if (language := detect_anchor_language(filenames)) is not None
123
+ if (
124
+ language := detect_anchor_language(
125
+ _anchor_names(dir_path, filenames, workspaces, root_path)
126
+ )
127
+ )
128
+ is not None
71
129
  )
72
130
 
73
131
 
@@ -187,7 +245,7 @@ def _build_single_root_tree(root_path: Path, listing: DirListing) -> PartTree:
187
245
 
188
246
 
189
247
  def decide_part_tree(root_path: Path, listing: DirListing) -> PartTree:
190
- potential_parts = _discover_potential_parts(listing)
248
+ potential_parts = _discover_potential_parts(listing, root_path)
191
249
  if not potential_parts:
192
250
  return _build_single_root_tree(root_path, listing)
193
251
 
@@ -1,16 +1,7 @@
1
1
  [importlinter]
2
- root_package = fluidattacks_core
2
+ root_package = fluidattacks_core.filesystem
3
3
  include_external_packages = True
4
4
 
5
- [importlinter:contract:portion]
6
- name = Single Portion
7
- type = layers
8
- containers =
9
- fluidattacks_core
10
- exhaustive = true
11
- layers =
12
- filesystem
13
-
14
5
  [importlinter:contract:dag]
15
6
  name = Direct Acyclic Graph
16
7
  type = layers
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fluidattacks_core_filesystem"
3
- version = "12.0.0"
3
+ version = "14.0.0"
4
4
  description = "Fluid Attacks Core Filesystem Library"
5
5
  authors = [{ name = "Development", email = "development@fluidattacks.com" }]
6
6
  license = { text = "MPL-2.0" }
@@ -5,6 +5,7 @@ import pytest
5
5
 
6
6
  from fluidattacks_core.filesystem import build_part_tree
7
7
  from fluidattacks_core.filesystem.partitioning import (
8
+ DirListing,
8
9
  PotentialPart,
9
10
  _assign_files_to_parts,
10
11
  _build_hierarchy,
@@ -28,6 +29,17 @@ _LISTING = (
28
29
  )
29
30
 
30
31
 
32
+ _GRADLE_ROOT = Path("/service")
33
+
34
+ _GRADLE_MODULES = (
35
+ "applications/app-service",
36
+ "domain/model",
37
+ "domain/usecase",
38
+ "infrastructure/driven-adapters/rest-consumer",
39
+ "infrastructure/entry-points/reactive-web",
40
+ )
41
+
42
+
31
43
  def test_part_names_are_posix_relative_paths() -> None:
32
44
  assert _generate_part_name(_ROOT, _ROOT) == "."
33
45
  assert _generate_part_name(_ROOT / "web", _ROOT) == "web"
@@ -41,7 +53,7 @@ def test_config_files_are_found_by_name_and_glob() -> None:
41
53
 
42
54
 
43
55
  def test_only_directories_holding_a_manifest_become_parts() -> None:
44
- discovered = _discover_potential_parts(_LISTING)
56
+ discovered = _discover_potential_parts(_LISTING, _ROOT)
45
57
  assert [(path, language) for path, _, language in discovered] == [
46
58
  (_ROOT, "Python"),
47
59
  (_ROOT / "web", "JavaScript"),
@@ -49,7 +61,7 @@ def test_only_directories_holding_a_manifest_become_parts() -> None:
49
61
 
50
62
 
51
63
  def test_a_lone_hint_does_not_make_a_potential_part() -> None:
52
- assert _discover_potential_parts(((_ROOT / "tools", ("mypy.ini", "tox.ini")),)) == ()
64
+ assert _discover_potential_parts(((_ROOT / "tools", ("mypy.ini", "tox.ini")),), _ROOT) == ()
53
65
 
54
66
 
55
67
  def test_hierarchy_nests_parts_under_their_nearest_ancestor() -> None:
@@ -189,6 +201,126 @@ def test_part_names_stay_unique_for_nested_directories() -> None:
189
201
  assert sorted(names) == [".", "a/svc", "b/svc"]
190
202
 
191
203
 
204
+ def _gradle_listing(suffix: str) -> DirListing:
205
+ return (
206
+ (_GRADLE_ROOT, (f"settings{suffix}", f"build{suffix}", "gradlew")),
207
+ *(
208
+ (_GRADLE_ROOT.joinpath(*module.split("/")), (f"build{suffix}",))
209
+ for module in _GRADLE_MODULES
210
+ ),
211
+ )
212
+
213
+
214
+ def test_a_settings_file_claims_every_subproject_manifest() -> None:
215
+ tree = decide_part_tree(_GRADLE_ROOT, _gradle_listing(".gradle"))
216
+
217
+ assert sorted(tree.parts) == ["."]
218
+ assert tree.children_map["."] == ()
219
+ root = tree.root
220
+ assert root is not None
221
+ assert root.detected_language == "Java"
222
+ assert Path("domain/model/build.gradle") in root.files
223
+
224
+
225
+ def test_the_kotlin_gradle_dsl_collapses_the_same_way() -> None:
226
+ tree = decide_part_tree(_GRADLE_ROOT, _gradle_listing(".gradle.kts"))
227
+
228
+ assert sorted(tree.parts) == ["."]
229
+ root = tree.root
230
+ assert root is not None
231
+ assert root.detected_language == "Kotlin"
232
+
233
+
234
+ def test_a_settings_file_claims_the_other_gradle_dsl() -> None:
235
+ listing = (
236
+ (_GRADLE_ROOT, ("settings.gradle",)),
237
+ (_GRADLE_ROOT / "mod", ("build.gradle.kts",)),
238
+ )
239
+
240
+ assert sorted(decide_part_tree(_GRADLE_ROOT, listing).parts) == ["."]
241
+
242
+
243
+ def test_a_nested_workspace_anchors_but_its_modules_do_not() -> None:
244
+ listing = (
245
+ (_GRADLE_ROOT, ("settings.gradle",)),
246
+ (_GRADLE_ROOT / "sub", ("settings.gradle", "build.gradle")),
247
+ (_GRADLE_ROOT / "sub" / "mod", ("build.gradle",)),
248
+ )
249
+
250
+ assert sorted(decide_part_tree(_GRADLE_ROOT, listing).parts) == [".", "sub"]
251
+
252
+
253
+ def test_a_claimed_directory_with_its_own_settings_still_anchors() -> None:
254
+ listing = (
255
+ (_GRADLE_ROOT, ("settings.gradle", "build.gradle")),
256
+ (_GRADLE_ROOT / "mod", ("build.gradle",)),
257
+ (_GRADLE_ROOT / "deployment" / "acceptance-test", ("settings.gradle", "build.gradle")),
258
+ )
259
+
260
+ assert sorted(decide_part_tree(_GRADLE_ROOT, listing).parts) == [
261
+ ".",
262
+ "deployment/acceptance-test",
263
+ ]
264
+
265
+
266
+ def test_a_workspace_does_not_claim_a_foreign_manifest() -> None:
267
+ listing = (
268
+ (_GRADLE_ROOT, ("settings.gradle", "build.gradle")),
269
+ (_GRADLE_ROOT / "frontend", ("build.gradle", "package.json")),
270
+ )
271
+
272
+ tree = decide_part_tree(_GRADLE_ROOT, listing)
273
+
274
+ assert sorted(tree.parts) == [".", "frontend"]
275
+ frontend = tree.get_part("frontend")
276
+ assert frontend is not None
277
+ assert frontend.detected_language == "JavaScript"
278
+
279
+
280
+ def test_a_gradle_module_without_a_settings_file_anchors_alone() -> None:
281
+ listing = (
282
+ (_GRADLE_ROOT, ("README",)),
283
+ (_GRADLE_ROOT / "deployment" / "acceptance-test", ("build.gradle",)),
284
+ )
285
+
286
+ tree = decide_part_tree(_GRADLE_ROOT, listing)
287
+
288
+ assert sorted(tree.parts) == [".", "deployment/acceptance-test"]
289
+
290
+
291
+ def test_an_xcode_bundle_does_not_anchor_beside_its_project() -> None:
292
+ listing = (
293
+ (_ROOT, ("pubspec.yaml",)),
294
+ (_ROOT / "app" / "ios", ("Podfile",)),
295
+ (_ROOT / "app" / "ios" / "Runner.xcodeproj", ("project.pbxproj",)),
296
+ (_ROOT / "app" / "ios" / "Runner.xcworkspace", ("project.pbxproj",)),
297
+ )
298
+
299
+ assert sorted(decide_part_tree(_ROOT, listing).parts) == [".", "app/ios"]
300
+
301
+
302
+ def test_a_manifest_nested_inside_a_bundle_does_not_anchor() -> None:
303
+ listing = (
304
+ (_ROOT, ("Podfile",)),
305
+ (_ROOT / "Runner.xcodeproj", ("project.pbxproj",)),
306
+ (_ROOT / "Runner.xcodeproj" / "project.xcworkspace", ("Package.swift",)),
307
+ )
308
+
309
+ assert sorted(decide_part_tree(_ROOT, listing).parts) == ["."]
310
+
311
+
312
+ def test_a_root_named_like_a_bundle_still_anchors() -> None:
313
+ root = Path("/Thing.xcodeproj")
314
+
315
+ assert sorted(decide_part_tree(root, ((root, ("project.pbxproj",)),)).parts) == ["."]
316
+
317
+
318
+ def test_a_swift_package_outside_a_bundle_still_anchors() -> None:
319
+ listing = ((_ROOT, ("README",)), (_ROOT / "lib", ("Package.swift",)))
320
+
321
+ assert sorted(decide_part_tree(_ROOT, listing).parts) == [".", "lib"]
322
+
323
+
192
324
  def _write(root: Path, files: list[str]) -> None:
193
325
  for name in files:
194
326
  path = root / name
@@ -6,6 +6,7 @@ import pytest
6
6
 
7
7
  from fluidattacks_core.filesystem import (
8
8
  Language,
9
+ build_part_tree,
9
10
  detect_root_language,
10
11
  find_parent_project,
11
12
  find_projects,
@@ -270,3 +271,12 @@ def test_no_marker_name_is_excluded_as_an_artifact() -> None:
270
271
  if any(fnmatch(name, pat) for pat in EXCLUDE_FILES)
271
272
  ]
272
273
  assert excluded == []
274
+
275
+
276
+ def test_find_projects_still_splits_a_gradle_workspace(tmp_path: Path) -> None:
277
+ _make(tmp_path, ["settings.gradle", "build.gradle", "mod/build.gradle"])
278
+ root = tmp_path.resolve()
279
+
280
+ assert _by_path(find_projects(tmp_path), root) == {".": "Java", "mod": "Java"}
281
+ assert find_parent_project(root / "mod" / "build.gradle", root) == (root / "mod", "Java")
282
+ assert sorted(build_part_tree(tmp_path).parts) == ["."]
@@ -178,7 +178,7 @@ wheels = [
178
178
 
179
179
  [[package]]
180
180
  name = "fluidattacks-core-filesystem"
181
- version = "12.0.0"
181
+ version = "14.0.0"
182
182
  source = { editable = "." }
183
183
 
184
184
  [package.dev-dependencies]