fluidattacks_core_filesystem 12.0.0__tar.gz → 13.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-13.0.0}/.gitignore +4 -0
  2. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/PKG-INFO +1 -1
  3. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/fluidattacks_core/filesystem/defaults.py +13 -0
  4. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/fluidattacks_core/filesystem/detection.py +22 -2
  5. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/fluidattacks_core/filesystem/partitioning.py +38 -2
  6. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/import-linter.cfg +1 -10
  7. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/pyproject.toml +1 -1
  8. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/tests/test_partitioning.py +99 -0
  9. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/tests/test_projects.py +10 -0
  10. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/uv.lock +1 -1
  11. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/.envrc +0 -0
  12. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/build/default.nix +0 -0
  13. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/build/filter.nix +0 -0
  14. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/flake.lock +0 -0
  15. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/flake.nix +0 -0
  16. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/fluidattacks_core/filesystem/__init__.py +0 -0
  17. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/fluidattacks_core/filesystem/language.py +0 -0
  18. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/fluidattacks_core/filesystem/parts.py +0 -0
  19. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/fluidattacks_core/filesystem/projects.py +0 -0
  20. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/fluidattacks_core/filesystem/py.typed +0 -0
  21. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/mypy.ini +0 -0
  22. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/ruff.toml +0 -0
  23. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/tests/__init__.py +0 -0
  24. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/tests/data/partitioning_snapshot.golden.json +0 -0
  25. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/tests/test_defaults.py +0 -0
  26. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/tests/test_extract_db.py +0 -0
  27. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/tests/test_language_detection.py +0 -0
  28. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.0.0}/tests/test_partitioning_snapshot.py +0 -0
  29. {fluidattacks_core_filesystem-12.0.0 → fluidattacks_core_filesystem-13.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: 13.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,19 @@ 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
+ WORKSPACE_MARKERS: dict[str, WorkspaceSpec] = {
32
+ "gradle": {
33
+ "declares": frozenset({"settings.gradle", "settings.gradle.kts"}),
34
+ "claims": frozenset({"build.gradle", "build.gradle.kts"}),
35
+ },
36
+ }
37
+
38
+
26
39
  CONFIG_MARKERS: dict[Language, MarkerSet] = {
27
40
  Language.Java: {
28
41
  "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)
@@ -10,7 +10,13 @@ from pathlib import Path
10
10
  from typing import TYPE_CHECKING
11
11
 
12
12
  from .defaults import EXTENDED_EXCLUDE_DIRS
13
- from .detection import detect_anchor_language, detect_languages_in_dir, is_config_marker
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,41 @@ def _root_filenames(listing: DirListing, root_path: Path) -> tuple[str, ...]:
63
69
  return ()
64
70
 
65
71
 
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 _anchor_names(
90
+ dir_path: Path,
91
+ filenames: Sequence[str],
92
+ workspaces: Mapping[Path, frozenset[str]],
93
+ ) -> tuple[str, ...]:
94
+ kinds = _claiming_kinds(dir_path, workspaces)
95
+ if not kinds:
96
+ return tuple(filenames)
97
+ return tuple(name for name in filenames if not is_claimed_manifest(name, kinds))
98
+
99
+
66
100
  def _discover_potential_parts(listing: DirListing) -> tuple[PotentialPart, ...]:
101
+ workspaces = _workspace_kinds_by_dir(listing)
67
102
  return tuple(
68
103
  (dir_path, _find_config_files(dir_path, filenames), language.value)
69
104
  for dir_path, filenames in listing
70
- if (language := detect_anchor_language(filenames)) is not None
105
+ if (language := detect_anchor_language(_anchor_names(dir_path, filenames, workspaces)))
106
+ is not None
71
107
  )
72
108
 
73
109
 
@@ -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 = "13.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"
@@ -189,6 +201,93 @@ 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
+
192
291
  def _write(root: Path, files: list[str]) -> None:
193
292
  for name in files:
194
293
  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 = "13.0.0"
182
182
  source = { editable = "." }
183
183
 
184
184
  [package.dev-dependencies]