diff --git a/src/opossum_lib/opossum_model.py b/src/opossum_lib/opossum_model.py new file mode 100644 index 0000000..b76c337 --- /dev/null +++ b/src/opossum_lib/opossum_model.py @@ -0,0 +1,310 @@ +# SPDX-FileCopyrightText: TNG Technology Consulting GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import uuid +from collections import defaultdict +from collections.abc import Iterable +from copy import deepcopy +from dataclasses import field +from enum import Enum, auto +from pathlib import PurePath +from typing import Literal + +from pydantic import BaseModel, ConfigDict + +import opossum_lib.opossum.opossum_file as opossum_file +from opossum_lib.opossum.opossum_file_content import OpossumFileContent +from opossum_lib.opossum.output_model import OpossumOutputFile + +type OpossumPackageIdentifier = str +type ResourcePath = str + + +def _convert_path_to_str(path: PurePath) -> str: + return str(path).replace("\\", "/") + + +def default_attribution_id_mapper() -> dict[OpossumPackage, str]: + return defaultdict(lambda: str(uuid.uuid4())) + + +class Opossum(BaseModel): + model_config = ConfigDict(frozen=True, extra="forbid") + scan_results: ScanResults + review_results: OpossumOutputFile | None = None + + def to_opossum_file_format(self) -> OpossumFileContent: + return OpossumFileContent( + input_file=self.scan_results.to_opossum_file_format(), + output_file=self.review_results, + ) + + +class ScanResults(BaseModel): + model_config = ConfigDict(frozen=True, extra="forbid") + metadata: Metadata + resources: list[Resource] + attribution_breakpoints: list[str] = [] + external_attribution_sources: dict[str, ExternalAttributionSource] = {} + frequent_licenses: list[FrequentLicense] | None = None + files_with_children: list[str] | None = None + base_urls_for_sources: BaseUrlsForSources | None = None + attribution_to_id: dict[OpossumPackage, str] = field( + default_factory=default_attribution_id_mapper + ) + + def to_opossum_file_format(self) -> opossum_file.OpossumInformation: + external_attributions, resources_to_attributions = ( + self.create_attribution_mapping(self.resources) + ) + frequent_licenses = None + if self.frequent_licenses: + frequent_licenses = [ + license.to_opossum_file_format() for license in self.frequent_licenses + ] + base_urls_for_sources = ( + self.base_urls_for_sources + and self.base_urls_for_sources.to_opossum_file_format() + ) + + external_attribution_sources = { + key: val.to_opossum_file_format() + for (key, val) in self.external_attribution_sources.items() + } + + return opossum_file.OpossumInformation( + metadata=self.metadata.to_opossum_file_format(), + resources={ + str(resource.path): resource.to_opossum_file_format() + for resource in self.resources + }, + external_attributions=external_attributions, + resources_to_attributions=resources_to_attributions, + attribution_breakpoints=deepcopy(self.attribution_breakpoints), + external_attribution_sources=external_attribution_sources, + frequent_licenses=frequent_licenses, + files_with_children=deepcopy(self.files_with_children), + base_urls_for_sources=base_urls_for_sources, + ) + + def create_attribution_mapping( + self, + root_nodes: list[Resource], + ) -> tuple[ + dict[opossum_file.OpossumPackageIdentifier, opossum_file.OpossumPackage], + dict[opossum_file.ResourcePath, list[opossum_file.OpossumPackageIdentifier]], + ]: + external_attributions: dict[ + opossum_file.OpossumPackageIdentifier, opossum_file.OpossumPackage + ] = {} + resources_to_attributions: dict[ + opossum_file.ResourcePath, list[opossum_file.OpossumPackageIdentifier] + ] = {} + + def process_node(node: Resource) -> None: + path = _convert_path_to_str(node.path) + if not path.startswith("/"): + # the / is required by OpossumUI + path = "/" + path + + node_attributions_by_id = { + self.get_attribution_key(a): a.to_opossum_file_format() + for a in node.attributions + } + external_attributions.update(node_attributions_by_id) + + if len(node_attributions_by_id) > 0: + resources_to_attributions[path] = list(node_attributions_by_id.keys()) + + for child in node.children.values(): + process_node(child) + + for root in root_nodes: + process_node(root) + + return external_attributions, resources_to_attributions + + def get_attribution_key( + self, attribution: OpossumPackage + ) -> OpossumPackageIdentifier: + id = self.attribution_to_id[attribution] + self.attribution_to_id[attribution] = id + return id + + +class ResourceType(Enum): + FILE = auto() + FOLDER = auto() + + +class Resource(BaseModel): + model_config = ConfigDict(frozen=False, extra="forbid") + path: PurePath + type: ResourceType | None = None + attributions: list[OpossumPackage] = [] + children: dict[str, Resource] = {} + + def to_opossum_file_format(self) -> opossum_file.ResourceInFile: + if self.children or self.type == ResourceType.FOLDER: + return { + _convert_path_to_str( + child.path.relative_to(self.path) + ): child.to_opossum_file_format() + for child in self.children.values() + } + else: + return 1 + + def add_resource(self, resource: Resource) -> None: + if not resource.path.is_relative_to(self.path): + raise RuntimeError( + f"The path {resource.path} is not a child of this node at {self.path}." + ) + remaining_path_parts = resource.path.relative_to(self.path).parts + if remaining_path_parts: + self._add_resource(resource, remaining_path_parts) + else: + self._update(resource) + + def _add_resource( + self, resource: Resource, remaining_path_parts: Iterable[str] + ) -> None: + if not remaining_path_parts: + self._update(resource) + return + next, *rest_parts = remaining_path_parts + if next not in self.children: + self.children[next] = Resource(path=self.path / next) + self.children[next]._add_resource(resource, rest_parts) + + def _update(self, other: Resource) -> None: + if self.path != other.path: + raise RuntimeError( + "Trying to merge nodes with different paths: " + + f"{self.path} vs. {other.path}" + ) + if self.type and other.type and self.type != other.type: + raise RuntimeError( + "Trying to merge incompatible node types. " + + f"Current node is {self.type}. Other is {other.type}" + ) + self.type = self.type or other.type + self.attributions.extend(other.attributions) + for key, child in other.children.items(): + if key in self.children: + self.children[key]._update(child) + else: + self.children[key] = child + + +class BaseUrlsForSources(BaseModel): + model_config = ConfigDict(frozen=True, extra="allow") + + def to_opossum_file_format(self) -> opossum_file.BaseUrlsForSources: + return opossum_file.BaseUrlsForSources(**self.model_dump()) + + +class FrequentLicense(BaseModel): + model_config = ConfigDict(frozen=True, extra="forbid") + full_name: str + short_name: str + default_text: str + + def to_opossum_file_format(self) -> opossum_file.FrequentLicense: + return opossum_file.FrequentLicense( + full_name=self.full_name, + short_name=self.short_name, + default_text=self.default_text, + ) + + +class SourceInfo(BaseModel): + model_config = ConfigDict(frozen=True, extra="forbid") + name: str + document_confidence: int | float | None = 0 + additional_name: str | None = None + + def to_opossum_file_format(self) -> opossum_file.SourceInfo: + return opossum_file.SourceInfo( + name=self.name, + document_confidence=self.document_confidence, + additional_name=self.additional_name, + ) + + +class OpossumPackage(BaseModel): + model_config = ConfigDict(frozen=True, extra="forbid") + source: SourceInfo + attribution_confidence: int | None = None + comment: str | None = None + package_name: str | None = None + package_version: str | None = None + package_namespace: str | None = None + package_type: str | None = None + package_purl_appendix: str | None = None + copyright: str | None = None + license_name: str | None = None + license_text: str | None = None + url: str | None = None + first_party: bool | None = None + exclude_from_notice: bool | None = None + pre_selected: bool | None = None + follow_up: Literal["FOLLOW_UP"] | None = None + origin_id: str | None = None + origin_ids: list[str] | None = None + criticality: Literal["high"] | Literal["medium"] | None = None + was_preferred: bool | None = None + + def to_opossum_file_format(self) -> opossum_file.OpossumPackage: + return opossum_file.OpossumPackage( + source=self.source.to_opossum_file_format(), + attribution_confidence=self.attribution_confidence, + comment=self.comment, + package_name=self.package_name, + package_version=self.package_version, + package_namespace=self.package_namespace, + package_type=self.package_type, + package_p_u_r_l_appendix=self.package_purl_appendix, + copyright=self.copyright, + license_name=self.license_name, + license_text=self.license_text, + url=self.url, + first_party=self.first_party, + exclude_from_notice=self.exclude_from_notice, + pre_selected=self.pre_selected, + follow_up=self.follow_up, + origin_id=self.origin_id, + origin_ids=self.origin_ids, + criticality=self.criticality, + was_preferred=self.was_preferred, + ) + + +class Metadata(BaseModel): + model_config = ConfigDict(frozen=True, extra="allow") + project_id: str + file_creation_date: str + project_title: str + project_version: str | None = None + expected_release_date: str | None = None + build_date: str | None = None + + def to_opossum_file_format(self) -> opossum_file.Metadata: + return opossum_file.Metadata(**self.model_dump()) + + +class ExternalAttributionSource(BaseModel): + model_config = ConfigDict(frozen=True, extra="forbid") + name: str + priority: int + is_relevant_for_preferred: bool | None = None + + def to_opossum_file_format(self) -> opossum_file.ExternalAttributionSource: + return opossum_file.ExternalAttributionSource( + name=self.name, + priority=self.priority, + is_relevant_for_preferred=self.is_relevant_for_preferred, + ) diff --git a/src/opossum_lib/scancode/convert_scancode_to_opossum.py b/src/opossum_lib/scancode/convert_scancode_to_opossum.py index 81db539..1a84a38 100644 --- a/src/opossum_lib/scancode/convert_scancode_to_opossum.py +++ b/src/opossum_lib/scancode/convert_scancode_to_opossum.py @@ -8,15 +8,10 @@ import sys import uuid -from opossum_lib.opossum.opossum_file import ( - Metadata, - OpossumInformation, -) +import opossum_lib.opossum_model as opossum_model from opossum_lib.opossum.opossum_file_content import OpossumFileContent from opossum_lib.scancode.model import Header, ScanCodeData from opossum_lib.scancode.resource_tree import ( - convert_to_opossum_resources, - create_attribution_mapping, scancode_to_file_tree, ) @@ -26,29 +21,21 @@ def convert_scancode_to_opossum(filename: str) -> OpossumFileContent: scancode_data = load_scancode_json(filename) - filetree = scancode_to_file_tree(scancode_data) - resources = convert_to_opossum_resources(filetree) - external_attributions, resources_to_attributions = create_attribution_mapping( - filetree - ) + resources = [scancode_to_file_tree(scancode_data)] scancode_header = extract_scancode_header(scancode_data, filename) - metadata = Metadata( + metadata = opossum_model.Metadata( project_id=str(uuid.uuid4()), file_creation_date=scancode_header.end_timestamp, project_title="ScanCode file", ) - return OpossumFileContent( - OpossumInformation( + return opossum_model.Opossum( + scan_results=opossum_model.ScanResults( metadata=metadata, resources=resources, - external_attributions=external_attributions, - resources_to_attributions=resources_to_attributions, - attribution_breakpoints=[], - external_attribution_sources={}, ) - ) + ).to_opossum_file_format() def load_scancode_json(filename: str) -> ScanCodeData: diff --git a/src/opossum_lib/scancode/helpers.py b/src/opossum_lib/scancode/helpers.py deleted file mode 100644 index 1ba1fd3..0000000 --- a/src/opossum_lib/scancode/helpers.py +++ /dev/null @@ -1,19 +0,0 @@ -# SPDX-FileCopyrightText: TNG Technology Consulting GmbH -# -# SPDX-License-Identifier: Apache-2.0 - - -import os.path - -from pydantic import BaseModel -from pydantic_core import SchemaValidator - - -def path_segments(path: str) -> list[str]: - path = os.path.normpath(path) - return path.split(os.sep) - - -def check_schema(model: BaseModel) -> None: - schema_validator = SchemaValidator(schema=model.__pydantic_core_schema__) - schema_validator.validate_python(model.__dict__) diff --git a/src/opossum_lib/scancode/resource_tree.py b/src/opossum_lib/scancode/resource_tree.py index ad1e898..89318e9 100644 --- a/src/opossum_lib/scancode/resource_tree.py +++ b/src/opossum_lib/scancode/resource_tree.py @@ -5,72 +5,32 @@ from __future__ import annotations -from os.path import relpath +from pathlib import PurePath -from pydantic import BaseModel - -from opossum_lib.opossum.opossum_file import ( - OpossumPackage, - OpossumPackageIdentifier, - ResourceInFile, - ResourcePath, - SourceInfo, -) +import opossum_lib.opossum_model as opossum_model from opossum_lib.scancode.constants import SCANCODE_SOURCE_NAME -from opossum_lib.scancode.helpers import check_schema, path_segments from opossum_lib.scancode.model import File, FileType, ScanCodeData -class ScanCodeFileTree(BaseModel): - file: File - children: dict[str, ScanCodeFileTree] = {} - - def get_path(self, path: list[str]) -> ScanCodeFileTree: - if len(path) == 0: - return self - next_segment, *rest = path - if next_segment not in self.children: - self.children[next_segment] = ScanCodeFileTree.model_construct(None) # type: ignore - return self.children[next_segment].get_path(rest) - - def revalidate(self) -> None: - check_schema(self) - for child in self.children.values(): - child.revalidate() - - -def scancode_to_file_tree(scancode_data: ScanCodeData) -> ScanCodeFileTree: - temp_root = ScanCodeFileTree.model_construct(file=None) # type: ignore +def scancode_to_file_tree(scancode_data: ScanCodeData) -> opossum_model.Resource: + temp_root = opossum_model.Resource(path=PurePath("")) for file in scancode_data.files: - segments = path_segments(file.path) - temp_root.get_path(segments).file = file + resource = opossum_model.Resource( + path=PurePath(file.path), + attributions=get_attribution_info(file), + type=convert_resource_type(file.type), + ) + temp_root.add_resource(resource) assert len(temp_root.children) == 1 - root = list(temp_root.children.values())[0] - check_schema(root) - return root - - -def convert_to_opossum_resources(root_node: ScanCodeFileTree) -> ResourceInFile: - def process_node(node: ScanCodeFileTree) -> ResourceInFile: - if node.file.type == FileType.FILE: - return 1 - else: - root_path = node.file.path - children = { - relpath(n.file.path, root_path): process_node(n) - for n in node.children.values() - } - return children - - return {root_node.file.path: process_node(root_node)} + return list(temp_root.children.values())[0] -def get_attribution_info(file: File) -> list[OpossumPackage]: +def get_attribution_info(file: File) -> list[opossum_model.OpossumPackage]: if file.type == FileType.DIRECTORY: return [] copyright = "\n".join(c.copyright for c in file.copyrights) - source_info = SourceInfo(name=SCANCODE_SOURCE_NAME) + source_info = opossum_model.SourceInfo(name=SCANCODE_SOURCE_NAME) attribution_infos = [] for license_detection in file.license_detections: @@ -78,10 +38,10 @@ def get_attribution_info(file: File) -> list[OpossumPackage]: max_score = max(m.score for m in license_detection.matches) attribution_confidence = int(max_score) - package = OpossumPackage( + package = opossum_model.OpossumPackage( source=source_info, - licenseName=license_name, - attributionConfidence=attribution_confidence, + license_name=license_name, + attribution_confidence=attribution_confidence, copyright=copyright, ) attribution_infos.append(package) @@ -89,34 +49,8 @@ def get_attribution_info(file: File) -> list[OpossumPackage]: return attribution_infos -def get_attribution_key(attribution: OpossumPackage) -> OpossumPackageIdentifier: - return f"{attribution.license_name}-{hash(attribution)}" - - -def create_attribution_mapping( - root_node: ScanCodeFileTree, -) -> tuple[ - dict[OpossumPackageIdentifier, OpossumPackage], - dict[ResourcePath, list[OpossumPackageIdentifier]], -]: - external_attributions: dict[OpossumPackageIdentifier, OpossumPackage] = {} - resources_to_attributions: dict[ResourcePath, list[OpossumPackageIdentifier]] = {} - - def process_node(node: ScanCodeFileTree) -> None: - # the / is required by OpossumUI - path = "/" + node.file.path - attributions = get_attribution_info(node.file) - - new_attributions_with_id = {get_attribution_key(a): a for a in attributions} - external_attributions.update(new_attributions_with_id) - - if len(new_attributions_with_id) > 0: - resources_to_attributions[path] = list(new_attributions_with_id.keys()) - - for child in node.children.values(): - process_node(child) - - for child in root_node.children.values(): - process_node(child) - - return external_attributions, resources_to_attributions +def convert_resource_type(file_type: FileType) -> opossum_model.ResourceType: + if file_type == FileType.FILE: + return opossum_model.ResourceType.FILE + else: + return opossum_model.ResourceType.FOLDER diff --git a/tests/test_scancode/model_helpers.py b/tests/test_scancode/model_helpers.py index d7b6162..40a3a1b 100644 --- a/tests/test_scancode/model_helpers.py +++ b/tests/test_scancode/model_helpers.py @@ -2,7 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 -from pathlib import Path + +from pathlib import PurePath from opossum_lib.scancode.model import ( Copyright, @@ -14,6 +15,16 @@ ) +def _create_reference_scancode_files() -> list[File]: + return [ + _create_file("A", FileType.DIRECTORY), + _create_file("A/B", FileType.DIRECTORY), + _create_file("A/file1", FileType.FILE), + _create_file("A/file2.txt", FileType.FILE), + _create_file("A/B/file3", FileType.FILE), + ] + + def _create_file( path: str, type: FileType, @@ -73,11 +84,11 @@ def _create_file( if copyrights is None: copyrights = [] if name is None: - name = Path(path).name + name = PurePath(path).name if base_name is None: - base_name = Path(Path(path).name).stem + base_name = PurePath(PurePath(path).name).stem if extension is None: - extension = Path(path).suffix + extension = PurePath(path).suffix return File( authors=authors, base_name=base_name, diff --git a/tests/test_scancode/test_resource_tree.py b/tests/test_scancode/test_resource_tree.py index fa9d327..70701c0 100644 --- a/tests/test_scancode/test_resource_tree.py +++ b/tests/test_scancode/test_resource_tree.py @@ -3,153 +3,21 @@ # SPDX-License-Identifier: Apache-2.0 from copy import deepcopy -from typing import Any -from unittest import mock -import pytest -from pydantic import ValidationError - -from opossum_lib.opossum.opossum_file import OpossumPackage, SourceInfo +from opossum_lib.opossum_model import OpossumPackage, SourceInfo from opossum_lib.scancode.constants import SCANCODE_SOURCE_NAME from opossum_lib.scancode.model import ( Copyright, - File, FileBasedLicenseDetection, FileType, Match, - ScanCodeData, ) from opossum_lib.scancode.resource_tree import ( - ScanCodeFileTree, - convert_to_opossum_resources, - create_attribution_mapping, get_attribution_info, - scancode_to_file_tree, ) from tests.test_scancode.model_helpers import _create_file -class TestRevalidate: - def test_successfully_revalidate_valid_file_tree(self) -> None: - dummy_file = _create_file("A", FileType.FILE) - valid_structure = ScanCodeFileTree( - file=dummy_file, - children={ - "A": ScanCodeFileTree(file=dummy_file), - "B": ScanCodeFileTree( - file=dummy_file, children={"C": ScanCodeFileTree(file=dummy_file)} - ), - }, - ) - valid_structure.revalidate() - - def test_fail_to_revalidate_file_tree_invalid_at_toplevel(self) -> None: - dummy_file = _create_file("A", FileType.FILE) - invalid_structure = ScanCodeFileTree.model_construct( - children={ - "A": ScanCodeFileTree(file=dummy_file), - "B": ScanCodeFileTree( - file=dummy_file, children={"C": ScanCodeFileTree(file=dummy_file)} - ), - }, - file=None, # type: ignore - ) - with pytest.raises(ValidationError): - invalid_structure.revalidate() - - def test_fail_to_revalidate_file_tree_invalid_only_at_lower_level(self) -> None: - dummy_file = _create_file("A", FileType.FILE) - invalid_structure = ScanCodeFileTree( - file=dummy_file, - children={ - "A": ScanCodeFileTree(file=dummy_file), - "B": ScanCodeFileTree( - file=dummy_file, - children={"C": ScanCodeFileTree.model_construct(None)}, # type: ignore - ), - }, - ) - with pytest.raises(ValidationError): - invalid_structure.revalidate() - - -def test_scancode_to_resource_tree_produces_expected_result() -> None: - files = _create_reference_scancode_files() - scancode_data = ScanCodeData( - headers=[], packages=[], dependencies=[], license_detections=[], files=files - ) - - tree = scancode_to_file_tree(scancode_data) - reference = _create_reference_node_structure() - - assert tree == reference - - -def test_convert_to_opossum_resources_produces_expected_result() -> None: - scancode_data = ScanCodeData( - headers=[], - packages=[], - dependencies=[], - license_detections=[], - files=_create_reference_scancode_files(), - ) - - tree = scancode_to_file_tree(scancode_data) - resources = convert_to_opossum_resources(tree) - reference = {"A": {"B": {"file3": 1}, "file1": 1, "file2.txt": 1}} - assert resources == reference - - -@mock.patch( - "opossum_lib.scancode.resource_tree.get_attribution_info", - autospec=True, - return_value=[OpossumPackage(source=SourceInfo(name="mocked"))], -) -def test_create_attribution_mapping_paths_have_root_prefix(_: Any) -> None: - rootnode = _create_reference_node_structure() - _, resources_to_attributions = create_attribution_mapping(rootnode) - # OpossumUI automatically prepends every path with a "/" - # So our resourcesToAttributions needs to start every path with "/" - # even though ScanCode paths don't start with "/". - assert "/A/file1" in resources_to_attributions - assert "/A/file2.txt" in resources_to_attributions - assert "/A/B/file3" in resources_to_attributions - - -def test_create_attribution_mapping() -> None: - _, _, file1, file2, file3 = _create_reference_scancode_files() - pkg1 = OpossumPackage(source=SourceInfo(name="S1")) - pkg2 = OpossumPackage(source=SourceInfo(name="S2")) - pkg3 = OpossumPackage(source=SourceInfo(name="S3")) - - def get_attribution_info_mock(file: File) -> list[OpossumPackage]: - if file == file1: - return [deepcopy(pkg1), deepcopy(pkg2)] - elif file == file2: - return [deepcopy(pkg1), deepcopy(pkg2), deepcopy(pkg3)] - elif file == file3: - return [] - else: - return [] - - root_node = _create_reference_node_structure() - - with mock.patch( - "opossum_lib.scancode.resource_tree.get_attribution_info", - new=get_attribution_info_mock, - ): - external_attributions, resources_to_attributions = create_attribution_mapping( - root_node - ) - assert len(external_attributions) == 3 # deduplication worked - - reverse_mapping = {v: k for (k, v) in external_attributions.items()} - id1, id2, id3 = reverse_mapping[pkg1], reverse_mapping[pkg2], reverse_mapping[pkg3] - assert len(resources_to_attributions) == 2 # only files with attributions - assert set(resources_to_attributions["/" + file1.path]) == {id1, id2} - assert set(resources_to_attributions["/" + file2.path]) == {id1, id2, id3} - - def test_get_attribution_info_directory() -> None: folder = _create_file("A", FileType.DIRECTORY) assert get_attribution_info(folder) == [] @@ -217,40 +85,14 @@ def test_get_attribution_info_file_multiple() -> None: attributions = get_attribution_info(file) expected1 = OpossumPackage( source=SourceInfo(name=SCANCODE_SOURCE_NAME), - licenseName="Apache-2.0", + license_name="Apache-2.0", copyright="Me\nMyself\nI", - attributionConfidence=95, + attribution_confidence=95, ) expected2 = OpossumPackage( source=SourceInfo(name=SCANCODE_SOURCE_NAME), - licenseName="MIT", + license_name="MIT", copyright="Me\nMyself\nI", - attributionConfidence=50, + attribution_confidence=50, ) assert set(attributions) == {expected1, expected2} - - -def _create_reference_scancode_files() -> list[File]: - return [ - _create_file("A", FileType.DIRECTORY), - _create_file("A/B", FileType.DIRECTORY), - _create_file("A/file1", FileType.FILE), - _create_file("A/file2.txt", FileType.FILE), - _create_file("A/B/file3", FileType.FILE), - ] - - -def _create_reference_node_structure() -> ScanCodeFileTree: - folder, subfolder, file1, file2, file3 = _create_reference_scancode_files() - inner = ScanCodeFileTree( - file=subfolder, children={"file3": ScanCodeFileTree(file=file3)} - ) - reference = ScanCodeFileTree( - file=folder, - children={ - "B": inner, - "file1": ScanCodeFileTree(file=file1), - "file2.txt": ScanCodeFileTree(file=file2), - }, - ) - return reference