diff --git a/alfred/apps.py b/alfred/apps.py index f40fdf418..6fa6518d5 100644 --- a/alfred/apps.py +++ b/alfred/apps.py @@ -2,16 +2,13 @@ import alfred +from writer import wf_project + @alfred.command('apps.update', help='update the apps in the repo to the latest version') @alfred.option('--app', help='the path of the app to update') def apps_update(app: str = None): import writer - from writer.core import ( - wf_project_migrate_obsolete_ui_json, - wf_project_read_files, - wf_project_write_files, - ) if app is not None: apps = [app] @@ -36,12 +33,12 @@ def apps_update(app: str = None): if os.path.isfile(os.path.join(abs_path, "ui.json")): print(f'{app} : migrate ui.json') - wf_project_migrate_obsolete_ui_json(abs_path) + wf_project.migrate_obsolete_ui_json(abs_path) - metadata, components = wf_project_read_files(abs_path) + metadata, components = wf_project.read_files(abs_path) if metadata.get('writer_version') == writer.VERSION: print("The app is already up to date") else: metadata['writer_version'] = writer.VERSION - wf_project_write_files(abs_path, metadata, components) + wf_project.write_files(abs_path, metadata, components) print(f"{app} : app is up to date") diff --git a/src/writer/app_runner.py b/src/writer/app_runner.py index c5368270f..97ae8e36a 100644 --- a/src/writer/app_runner.py +++ b/src/writer/app_runner.py @@ -18,16 +18,8 @@ from pydantic import ValidationError from watchdog.observers.polling import PollingObserver -from writer import VERSION, audit_and_fix -from writer.core import ( - EventHandlerRegistry, - MiddlewareRegistry, - WriterSession, - use_request_context, - wf_project_migrate_obsolete_ui_json, - wf_project_read_files, - wf_project_write_files, -) +from writer import VERSION, audit_and_fix, wf_project +from writer.core import EventHandlerRegistry, MiddlewareRegistry, WriterSession, use_request_context from writer.core_ui import ingest_bmc_component_tree from writer.ss_types import ( AppProcessServerRequest, @@ -678,25 +670,27 @@ async def dispatch_message(self, session_id: Optional[str], request: AppProcessS return response def _load_persisted_script(self) -> str: + logger = logging.getLogger('writer') try: contents = None with open(os.path.join(self.app_path, "main.py"), "r", encoding='utf-8') as f: contents = f.read() return contents except FileNotFoundError: - logging.error( + logger.error( "Couldn't find main.py in the path provided: %s.", self.app_path) sys.exit(1) def _load_persisted_components(self) -> Dict[str, ComponentDefinition]: + logger = logging.getLogger('writer') if os.path.isfile(os.path.join(self.app_path, "ui.json")): - wf_project_migrate_obsolete_ui_json(self.app_path) + wf_project.migrate_obsolete_ui_json(self.app_path) if not os.path.isdir(os.path.join(self.app_path, ".wf")): - logging.error("Couldn't find .wf in the path provided: %s.", self.app_path) + logger.error("Couldn't find .wf in the path provided: %s.", self.app_path) sys.exit(1) - _, components = wf_project_read_files(self.app_path) + _, components = wf_project.read_files(self.app_path) components = audit_and_fix.fix_components(components) return components @@ -720,7 +714,7 @@ async def update_components(self, session_id: str, payload: ComponentUpdateReque "Cannot update components in non-update mode.") self.bmc_components = payload.components - wf_project_write_files(self.app_path, metadata={"writer_version": VERSION}, components=payload.components) + wf_project.write_files(self.app_path, metadata={"writer_version": VERSION}, components=payload.components) return await self.dispatch_message(session_id, ComponentUpdateRequest( type="componentUpdate", diff --git a/src/writer/command_line.py b/src/writer/command_line.py index db1bdd6d3..95d5da7cf 100644 --- a/src/writer/command_line.py +++ b/src/writer/command_line.py @@ -7,11 +7,6 @@ import click import writer.serve -from writer.core import ( - wf_project_migrate_obsolete_ui_json, - wf_project_read_files, - wf_project_write_files, -) from writer.deploy import cloud CONTEXT_SETTINGS = {'help_option_names': ['-h', '--help']} diff --git a/src/writer/core.py b/src/writer/core.py index 0b45c2ec8..d65ab41df 100644 --- a/src/writer/core.py +++ b/src/writer/core.py @@ -11,12 +11,10 @@ import logging import math import multiprocessing -import os import re import secrets import time import traceback -import types import urllib.request from abc import ABCMeta from contextvars import ContextVar @@ -47,13 +45,11 @@ from writer import core_ui from writer.core_ui import Component from writer.ss_types import ( - ComponentDefinition, DataframeRecordAdded, DataframeRecordRemoved, DataframeRecordUpdated, InstancePath, InstancePathItem, - MetadataDefinition, Readable, WriterEvent, WriterEventResult, @@ -2423,107 +2419,6 @@ def writer_event_handler_build_arguments(func: Callable, writer_args: dict) -> L return func_args -def wf_project_write_files(app_path: str, metadata: MetadataDefinition, components: dict[str, ComponentDefinition]) -> None: - """ - Writes the meta data of the WF project to the `.wf` directory (metadata, components, ...). - - * the metadata.json file is written in json format - * a file for the root component written in jsonline format - * one file per page is created in the form `components-{id}.json` in jsonline format - - >>> wf_project_write_files('app/hello', metadata={"writer_version": "0.1" }, components=...) - """ - wf_directory = os.path.join(app_path, ".wf") - if not os.path.exists(wf_directory): - os.makedirs(wf_directory) - - with io.open(os.path.join(wf_directory, "metadata.json"), "w") as f: - json.dump(metadata, f, indent=4) - - root_component = components["root"] - with io.open(os.path.join(wf_directory, "components-root.jsonl"), "w") as f: - f.write(json.dumps(root_component)) - - list_pages = [] - for c in components.values(): - if c["type"] == "page": - list_pages.append(c["id"]) - - for position, page_id in enumerate(list_pages): - page_components = [] - page_components_ids = {page_id} - for c in components.values(): - if _lookup_page_for_component(components, c['id']) in page_components_ids: - page_components.append(c) - page_components_ids.add(c["id"]) - - with io.open(os.path.join(wf_directory, f"components-page-{position}-{page_id}.jsonl"), "w") as f: - for p in page_components: - f.write(json.dumps(p) + "\n") - -def wf_project_read_files(app_path: str) -> Tuple[MetadataDefinition, dict[str, ComponentDefinition]]: - """ - Reads project files in the `.wf` folder. - - The components are read in page order. - - >>> metadata, components = wf_project_read_files('app/hello') - """ - components: dict[str, ComponentDefinition] = {} - - meta_data_path = os.path.join(app_path, ".wf", "metadata.json") - try: - with io.open(meta_data_path, "r") as filep: - metadata: MetadataDefinition = json.load(filep) - except Exception as e: - raise ValueError(f"Error reading metadata file {meta_data_path} : {e}") - - root_component_path = os.path.join(app_path, ".wf", "components-root.jsonl") - try: - with io.open(root_component_path, "r") as filep: - root_component: ComponentDefinition = json.loads(filep.read()) - components.update({root_component["id"]: root_component}) - except Exception as e: - raise ValueError(f"Error reading root component file {root_component_path} : {e}") - - files = os.listdir(os.path.join(app_path, ".wf")) - page_files = [file for file in files if file.startswith("components-page-")] - sorted_page_files = sorted(page_files, key=lambda x: int(x.split("-")[2])) - for page_file in sorted_page_files: - page_file_path = os.path.join(app_path, ".wf", page_file) - try: - with io.open(page_file_path, "r") as filep: - for line in filep: - component: ComponentDefinition = json.loads(line) - components.update({component["id"]: component}) - except Exception as e: - raise ValueError(f"Error reading page component file {page_file_path} : {e}") - - return metadata, components - - -def wf_project_migrate_obsolete_ui_json(app_path: str) -> None: - """ - Migrates a project that uses ui.json file to the current project format - - The ui.json file is removed after the migration. - """ - assert os.path.isfile(os.path.join(app_path, "ui.json")), f"ui.json file required for migration into {app_path}" - - logger = logging.getLogger('writer') - with io.open(os.path.join(app_path, "ui.json"), "r") as f: - parsed_file = json.load(f) - - if not isinstance(parsed_file, dict): - raise ValueError("No dictionary found in components file.") - - file_payload = parsed_file - metadata = file_payload.get("metadata", {}) - components = file_payload.get("components", {}) - wf_project_write_files(app_path, metadata, components) - os.remove(os.path.join(app_path, "ui.json")) - logger.warning('project format has changed and has been migrated with success. ui.json file has been removed.') - def writer_event_handler_invoke(callable_handler: Callable, writer_args: dict) -> Any: """ @@ -2637,29 +2532,6 @@ def _event_handler_ui_manager(): return WriterUIManager() -def _lookup_page_for_component(components: dict[str, ComponentDefinition], component_id: str) -> Optional[str]: - """ - Retrieves the page of a component - - >>> _lookup_page_for_component(components, "6a490318-239e-4fe9-a56b-f0f33d628c87") - """ - component = components[component_id] - parent_id = component.get("parentId") - if parent_id is None: - return None - - if component['type'] == "page": - return component['id'] - - if parent_id in components: - if components[parent_id]['type'] == "page": - return parent_id - else: - return _lookup_page_for_component(components, parent_id) - - return None - - def _split_record_as_pandas_record_and_index(param: dict, index_columns: list) -> Tuple[dict, tuple]: """ Separates a record into the record part and the index part to be able to diff --git a/src/writer/core_ui.py b/src/writer/core_ui.py index f61ecd4f4..296af6a86 100644 --- a/src/writer/core_ui.py +++ b/src/writer/core_ui.py @@ -9,6 +9,8 @@ from pydantic import BaseModel, Field from typing_extensions import TypedDict +from writer.ss_types import ComponentDefinition + current_parent_container: ContextVar[Union["Component", None]] = \ ContextVar("current_parent_container") @@ -384,6 +386,32 @@ def ingest_bmc_component_tree(component_tree: ComponentTree, components: Dict[st component_tree.ingest(components, tree=Branch.bmc) +def lookup_page_for_component(components: Dict[str, ComponentDefinition], component_id: str) -> Optional[str]: + """ + Retrieves the page of a component + + >>> lookup_page_for_component(components, "6a490318-239e-4fe9-a56b-f0f33d628c87") + """ + component: Optional[ComponentDefinition] = components.get(component_id, None) + if component is None: + return None + + parent_id = component.get("parentId") + if parent_id is None: + return None + + if component['type'] == "page": + return component['id'] + + if parent_id in components: + if components[parent_id]['type'] == "page": + return parent_id + else: + return lookup_page_for_component(components, parent_id) + + return None + + def cmc_components_list(component_tree: ComponentTree) -> list: """ Returns the list of code managed components in the component tree. diff --git a/src/writer/wf_project.py b/src/writer/wf_project.py new file mode 100644 index 000000000..f5b6eaa18 --- /dev/null +++ b/src/writer/wf_project.py @@ -0,0 +1,120 @@ +""" +This module manipulates the folder of a wf project stored into `wf`. + +>>> wf_project.write_files('app/hello', metadata={"writer_version": "0.1" }, components=...) + +>>> metadata, components = wf_project.read_files('app/hello') +""" +import io +import json +import logging +import os +from typing import Tuple + +from writer import core_ui +from writer.ss_types import ComponentDefinition, MetadataDefinition + + +def write_files(app_path: str, metadata: MetadataDefinition, components: dict[str, ComponentDefinition]) -> None: + """ + Writes the meta data of the WF project to the `.wf` directory (metadata, components, ...). + + * the metadata.json file is written in json format + * a file for the root component written in jsonline format + * one file per page is created in the form `components-{id}.json` in jsonline format + + >>> wf_project.write_files('app/hello', metadata={"writer_version": "0.1" }, components=...) + """ + wf_directory = os.path.join(app_path, ".wf") + if not os.path.exists(wf_directory): + os.makedirs(wf_directory) + + with io.open(os.path.join(wf_directory, "metadata.json"), "w") as f: + json.dump(metadata, f, indent=4) + + root_component = components["root"] + with io.open(os.path.join(wf_directory, "components-root.jsonl"), "w") as f: + f.write(json.dumps(root_component)) + + list_pages = [] + for c in components.values(): + if c["type"] == "page": + list_pages.append(c["id"]) + + for position, page_id in enumerate(list_pages): + page_components = [] + page_components_ids = {page_id} + for c in components.values(): + if core_ui.lookup_page_for_component(components, c['id']) in page_components_ids: + page_components.append(c) + page_components_ids.add(c["id"]) + + with io.open(os.path.join(wf_directory, f"components-page-{position}-{page_id}.jsonl"), "w") as f: + for p in page_components: + f.write(json.dumps(p) + "\n") + +def read_files(app_path: str) -> Tuple[MetadataDefinition, dict[str, ComponentDefinition]]: + """ + Reads project files in the `.wf` folder. + + The components are read in page order. + + >>> metadata, components = wf_project.read_files('app/hello') + """ + components: dict[str, ComponentDefinition] = {} + + meta_data_path = os.path.join(app_path, ".wf", "metadata.json") + try: + with io.open(meta_data_path, "r") as filep: + metadata: MetadataDefinition = json.load(filep) + except Exception as e: + raise ValueError(f"Error reading metadata file {meta_data_path} : {e}") + + root_component_path = os.path.join(app_path, ".wf", "components-root.jsonl") + try: + with io.open(root_component_path, "r") as filep: + root_component: ComponentDefinition = json.loads(filep.read()) + components.update({root_component["id"]: root_component}) + except Exception as e: + raise ValueError(f"Error reading root component file {root_component_path} : {e}") + + files = os.listdir(os.path.join(app_path, ".wf")) + page_files = [file for file in files if file.startswith("components-page-")] + sorted_page_files = sorted(page_files, key=lambda x: int(x.split("-")[2])) + for page_file in sorted_page_files: + page_file_path = os.path.join(app_path, ".wf", page_file) + try: + with io.open(page_file_path, "r") as filep: + for line in filep: + component: ComponentDefinition = json.loads(line) + components.update({component["id"]: component}) + except Exception as e: + raise ValueError(f"Error reading page component file {page_file_path} : {e}") + + return metadata, components + + +def migrate_obsolete_ui_json(app_path: str) -> None: + """ + Migrates a project that uses ui.json file to the current project format + + The ui.json file is removed after the migration. + + >>> wf_project.migrate_obsolete_ui_json('app/hello') + """ + assert os.path.isfile(os.path.join(app_path, "ui.json")), f"ui.json file required for migration into {app_path}" + + logger = logging.getLogger('writer') + with io.open(os.path.join(app_path, "ui.json"), "r") as f: + parsed_file = json.load(f) + + if not isinstance(parsed_file, dict): + raise ValueError("No dictionary found in components file.") + + file_payload = parsed_file + metadata = file_payload.get("metadata", {}) + components = file_payload.get("components", {}) + write_files(app_path, metadata, components) + os.remove(os.path.join(app_path, "ui.json")) + logger.warning('project format has changed and has been migrated with success. ui.json file has been removed.') + diff --git a/tests/backend/test_core.py b/tests/backend/test_core.py index 355048095..39c008b6f 100644 --- a/tests/backend/test_core.py +++ b/tests/backend/test_core.py @@ -1,11 +1,8 @@ import json import math -import os.path -import shutil -import tempfile import unittest import urllib -from typing import Dict, List +from typing import Dict import altair import numpy as np @@ -17,7 +14,7 @@ import pyarrow as pa import pytest import writer as wf -from writer import audit_and_fix +from writer import audit_and_fix, wf_project from writer.core import ( BytesWrapper, Evaluator, @@ -31,14 +28,11 @@ WriterState, import_failure, parse_state_variable_expression, - wf_project_migrate_obsolete_ui_json, - wf_project_read_files, - wf_project_write_files, ) from writer.core_ui import Component -from writer.ss_types import ComponentDefinition, WriterEvent +from writer.ss_types import WriterEvent -from tests.backend import test_app_dir, testobsoleteapp +from tests.backend import test_app_dir from tests.backend.fixtures import ( core_ui_fixtures, file_fixtures, @@ -74,7 +68,7 @@ wf.Config.is_mail_enabled_for_log = True wf.init_state(raw_state_dict) -_, sc = wf_project_read_files(test_app_dir) +_, sc = wf_project.read_files(test_app_dir) sc = audit_and_fix.fix_components(sc) session = wf.session_manager.get_new_session() @@ -1717,102 +1711,3 @@ def test_parse_state_variable_expression_should_process_expression(): assert parse_state_variable_expression('features\.eyes') == ['features.eyes'] assert parse_state_variable_expression('features\.eyes.color') == ['features.eyes', 'color'] - -def test_wf_project_write_files_should_write_metadatajson_in_wf_directory(): - """ - Tests that the function writes the meta information to the .wf configuration folder - with a metadata.json file and a file for the root and a file per page. - - """ - with tempfile.TemporaryDirectory('test_wf_project_write_files') as test_app_dir: - # Given - components_root: List[ComponentDefinition] = load_fixture_content('components/components-root.jsonl') - components = {c['id']: c for c in components_root} - - # When - wf_project_write_files(test_app_dir, metadata={'writer_version': '0.1.0'}, components=components) - - # Then - assert os.path.isfile(os.path.join(test_app_dir, '.wf', 'metadata.json')) - metadata: dict = file_fixtures.read(os.path.join(test_app_dir, '.wf', 'metadata.json')) - assert metadata['writer_version'] == '0.1.0' - -def test_wf_project_write_files_should_write_components_files_in_wf_directory(): - """ - Tests that the wf_project_write_files function writes the `components-.jsonl` files to the `.wf` directory - - * the components-root.jsonl file is written - * the components-page-0-bb4d0e86-619e-4367-a180-be28ab6059f4.jsonl file is written - """ - with tempfile.TemporaryDirectory('test_wf_project_write_files') as test_app_dir: - # Given - components_root: List[ComponentDefinition] = load_fixture_content('components/components-root.jsonl') - components = {c['id']: c for c in components_root } - - component_page: List[ComponentDefinition] = load_fixture_content('components/components-page-0.jsonl') - components.update({c['id']: c for c in component_page}) - - # When - wf_project_write_files(test_app_dir, metadata={'writer_version': '0.1.0'}, components=components) - - # Then - assert os.path.isfile(os.path.join(test_app_dir, '.wf', 'components-root.jsonl')) - root: List[ComponentDefinition] = file_fixtures.read(os.path.join(test_app_dir, '.wf', 'components-root.jsonl')) - assert root[0] == components_root[0] - - assert os.path.isfile(os.path.join(test_app_dir, '.wf', 'components-page-0-bb4d0e86-619e-4367-a180-be28ab6059f4.jsonl')) - page: List[ComponentDefinition] = file_fixtures.read(os.path.join(test_app_dir, '.wf', 'components-page-0-bb4d0e86-619e-4367-a180-be28ab6059f4.jsonl')) - assert page[0] == component_page[0] - -def test_wf_project_write_files_should_write_preserve_page_order_in_wf_directory(): - """ - Tests that wf_project_write_files preserves the order of pages defined in the component structure - - * the components of `components-page-1.jsonl` loaded first are written to the file `components-page-0-23bc1387-26ed-4ff2-8565-b027c2960c3c.jsonl` - * the components of `components-page-0.jsonl` loaded second are written in the file `components-page-1-bb4d0e86-619e-4367-a180-be28ab6059f4.jsonl` - - """ - with tempfile.TemporaryDirectory('test_wf_project_write_files') as test_app_dir: - # Given - components_root: List[ComponentDefinition] = load_fixture_content('components/components-root.jsonl') - components = {c['id']: c for c in components_root } - - components_page_0: List[ComponentDefinition] = load_fixture_content('components/components-page-1.jsonl') - components.update({c['id']: c for c in components_page_0}) - - components_page_1: List[ComponentDefinition] = load_fixture_content('components/components-page-0.jsonl') - components.update({c['id']: c for c in components_page_1}) - - # When - wf_project_write_files(test_app_dir, metadata={'writer_version': '0.1.0'}, components=components) - - # Then - assert os.path.isfile(os.path.join(test_app_dir, '.wf', 'components-page-0-23bc1387-26ed-4ff2-8565-b027c2960c3c.jsonl')) - page_0: List[ComponentDefinition] = file_fixtures.read(os.path.join(test_app_dir, '.wf', 'components-page-0-23bc1387-26ed-4ff2-8565-b027c2960c3c.jsonl')) - assert page_0[0] == components_page_0[0] - - assert os.path.isfile(os.path.join(test_app_dir, '.wf', 'components-page-1-bb4d0e86-619e-4367-a180-be28ab6059f4.jsonl')) - page_1: List[ComponentDefinition] = file_fixtures.read(os.path.join(test_app_dir, '.wf', 'components-page-1-bb4d0e86-619e-4367-a180-be28ab6059f4.jsonl')) - assert page_1[0] == components_page_1[0] - - -def test_wf_project_read_files_should_read_files_in_wf_directory(): - # When - metadata, sc = wf_project_read_files(test_app_dir) - - # Then - assert 'writer_version' in metadata - assert isinstance(sc, dict) - assert len(sc) != 0 - -def test_wf_project_migrate_obsolete_ui_json_should_migrate_ui_json_into_wf_directory(): - with tempfile.TemporaryDirectory('wf_project_migrate_obsolete_ui_json') as tmp_app_dir: - shutil.copytree(testobsoleteapp, tmp_app_dir, dirs_exist_ok=True) - - # When - wf_project_migrate_obsolete_ui_json(tmp_app_dir) - - # Then - assert not os.path.isfile(os.path.join(tmp_app_dir, 'ui.json')) - assert os.path.isfile(os.path.join(tmp_app_dir, '.wf', 'metadata.json')) - assert os.path.isfile(os.path.join(tmp_app_dir, '.wf', 'components-root.jsonl')) diff --git a/tests/backend/test_core_ui.py b/tests/backend/test_core_ui.py new file mode 100644 index 000000000..72dc59bce --- /dev/null +++ b/tests/backend/test_core_ui.py @@ -0,0 +1,30 @@ +from typing import List + +from writer import core_ui +from writer.ss_types import ComponentDefinition + +from backend.fixtures import load_fixture_content + + +def test_lookup_page_for_component_should_retrieve_the_page_for_a_specific_component_id(): + # Given + components_list: List[ComponentDefinition] = load_fixture_content('components/components-page-1.jsonl') + components = {c['id']: c for c in components_list} + + # When + page_id = core_ui.lookup_page_for_component(components, '42ab5c3d-21fc-4e88-befd-33e52fd15e8b') + + # Then + assert page_id == '23bc1387-26ed-4ff2-8565-b027c2960c3c' + + +def test_lookup_page_for_component_should_return_nothing_when_the_component_match_nothing(): + # Given + components_list: List[ComponentDefinition] = load_fixture_content('components/components-page-1.jsonl') + components = {c['id']: c for c in components_list} + + # When + page_id = core_ui.lookup_page_for_component(components, 'xxxxxxxxx-xxxxx-4e88-befd-33e52fd15e8b') + + # Then + assert page_id is None diff --git a/tests/backend/test_ui.py b/tests/backend/test_ui.py index 748881a97..106a29809 100644 --- a/tests/backend/test_ui.py +++ b/tests/backend/test_ui.py @@ -2,8 +2,7 @@ import json import writer as wf -from writer import audit_and_fix, core -from writer.core import wf_project_read_files +from writer import audit_and_fix, core, wf_project from writer.core_ui import ( Component, UIError, @@ -17,7 +16,7 @@ from backend.fixtures import core_ui_fixtures from tests.backend import test_app_dir -_, sc = wf_project_read_files(test_app_dir) +_, sc = wf_project.read_files(test_app_dir) sc = audit_and_fix.fix_components(sc) @contextlib.contextmanager diff --git a/tests/backend/test_wf_project.py b/tests/backend/test_wf_project.py new file mode 100644 index 000000000..f21a70e79 --- /dev/null +++ b/tests/backend/test_wf_project.py @@ -0,0 +1,110 @@ +import os +import shutil +import tempfile +from typing import List + +from writer import wf_project +from writer.ss_types import ComponentDefinition + +from tests.backend import test_app_dir, testobsoleteapp +from tests.backend.fixtures import file_fixtures, load_fixture_content + + +def test_wf_project_write_files_should_write_metadatajson_in_wf_directory(): + """ + Tests that the function writes the meta information to the .wf configuration folder + with a metadata.json file and a file for the root and a file per page. + + """ + with tempfile.TemporaryDirectory('test_wf_project_write_files') as test_app_dir: + # Given + components_root: List[ComponentDefinition] = load_fixture_content('components/components-root.jsonl') + components = {c['id']: c for c in components_root} + + # When + wf_project.write_files(test_app_dir, metadata={'writer_version': '0.1.0'}, components=components) + + # Then + assert os.path.isfile(os.path.join(test_app_dir, '.wf', 'metadata.json')) + metadata: dict = file_fixtures.read(os.path.join(test_app_dir, '.wf', 'metadata.json')) + assert metadata['writer_version'] == '0.1.0' + +def test_wf_project_write_files_should_write_components_files_in_wf_directory(): + """ + Tests that the wf_project_write_files function writes the `components-.jsonl` files to the `.wf` directory + + * the components-root.jsonl file is written + * the components-page-0-bb4d0e86-619e-4367-a180-be28ab6059f4.jsonl file is written + """ + with tempfile.TemporaryDirectory('test_wf_project_write_files') as test_app_dir: + # Given + components_root: List[ComponentDefinition] = load_fixture_content('components/components-root.jsonl') + components = {c['id']: c for c in components_root } + + component_page: List[ComponentDefinition] = load_fixture_content('components/components-page-0.jsonl') + components.update({c['id']: c for c in component_page}) + + # When + wf_project.write_files(test_app_dir, metadata={'writer_version': '0.1.0'}, components=components) + + # Then + assert os.path.isfile(os.path.join(test_app_dir, '.wf', 'components-root.jsonl')) + root: List[ComponentDefinition] = file_fixtures.read(os.path.join(test_app_dir, '.wf', 'components-root.jsonl')) + assert root[0] == components_root[0] + + assert os.path.isfile(os.path.join(test_app_dir, '.wf', 'components-page-0-bb4d0e86-619e-4367-a180-be28ab6059f4.jsonl')) + page: List[ComponentDefinition] = file_fixtures.read(os.path.join(test_app_dir, '.wf', 'components-page-0-bb4d0e86-619e-4367-a180-be28ab6059f4.jsonl')) + assert page[0] == component_page[0] + +def test_wf_project_write_files_should_write_preserve_page_order_in_wf_directory(): + """ + Tests that wf_project_write_files preserves the order of pages defined in the component structure + + * the components of `components-page-1.jsonl` loaded first are written to the file `components-page-0-23bc1387-26ed-4ff2-8565-b027c2960c3c.jsonl` + * the components of `components-page-0.jsonl` loaded second are written in the file `components-page-1-bb4d0e86-619e-4367-a180-be28ab6059f4.jsonl` + + """ + with tempfile.TemporaryDirectory('test_wf_project_write_files') as test_app_dir: + # Given + components_root: List[ComponentDefinition] = load_fixture_content('components/components-root.jsonl') + components = {c['id']: c for c in components_root } + + components_page_0: List[ComponentDefinition] = load_fixture_content('components/components-page-1.jsonl') + components.update({c['id']: c for c in components_page_0}) + + components_page_1: List[ComponentDefinition] = load_fixture_content('components/components-page-0.jsonl') + components.update({c['id']: c for c in components_page_1}) + + # When + wf_project.write_files(test_app_dir, metadata={'writer_version': '0.1.0'}, components=components) + + # Then + assert os.path.isfile(os.path.join(test_app_dir, '.wf', 'components-page-0-23bc1387-26ed-4ff2-8565-b027c2960c3c.jsonl')) + page_0: List[ComponentDefinition] = file_fixtures.read(os.path.join(test_app_dir, '.wf', 'components-page-0-23bc1387-26ed-4ff2-8565-b027c2960c3c.jsonl')) + assert page_0[0] == components_page_0[0] + + assert os.path.isfile(os.path.join(test_app_dir, '.wf', 'components-page-1-bb4d0e86-619e-4367-a180-be28ab6059f4.jsonl')) + page_1: List[ComponentDefinition] = file_fixtures.read(os.path.join(test_app_dir, '.wf', 'components-page-1-bb4d0e86-619e-4367-a180-be28ab6059f4.jsonl')) + assert page_1[0] == components_page_1[0] + + +def test_wf_project_read_files_should_read_files_in_wf_directory(): + # When + metadata, sc = wf_project.read_files(test_app_dir) + + # Then + assert 'writer_version' in metadata + assert isinstance(sc, dict) + assert len(sc) != 0 + +def test_wf_project_migrate_obsolete_ui_json_should_migrate_ui_json_into_wf_directory(): + with tempfile.TemporaryDirectory('wf_project_migrate_obsolete_ui_json') as tmp_app_dir: + shutil.copytree(testobsoleteapp, tmp_app_dir, dirs_exist_ok=True) + + # When + wf_project.migrate_obsolete_ui_json(tmp_app_dir) + + # Then + assert not os.path.isfile(os.path.join(tmp_app_dir, 'ui.json')) + assert os.path.isfile(os.path.join(tmp_app_dir, '.wf', 'metadata.json')) + assert os.path.isfile(os.path.join(tmp_app_dir, '.wf', 'components-root.jsonl'))