Skip to content

Commit

Permalink
new: Basic implementation of saving files created by the processor
Browse files Browse the repository at this point in the history
  • Loading branch information
JhnW committed Nov 10, 2024
1 parent 4de8da4 commit 6ad6f77
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 6 deletions.
2 changes: 1 addition & 1 deletion src/devana/code_generation/printers/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def filter_namespaces(a: Attribute) -> bool:

@dataclass
class PrinterConfiguration:
"""Data structure that stores standard code printing settings e.g. newline format."""
"""Data structure that stores standard code printing settings, e.g. newline format."""

line_ending: LineEndings = LineEndings.default()
indent: Indent = field(default_factory=lambda: Indent())
Expand Down
2 changes: 1 addition & 1 deletion src/devana/code_generation/printers/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def indent(self) -> Indent:
return self._cfg.indent

def print_line(self, text):
"""Format next part of text and accumulate this to text."""
"""Format the next part of text and accumulate this to text."""
self._text += self._cfg.format_line(text)

def clear(self):
Expand Down
1 change: 1 addition & 0 deletions src/devana/preprocessing/components/savers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Classes that implement the X interface to provide file writing functionality."""
63 changes: 63 additions & 0 deletions src/devana/preprocessing/components/savers/file_saver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from typing import Type, Iterable, Optional, Callable
from dataclasses import dataclass
from abc import ABC, abstractmethod
from pathlib import Path
from devana.preprocessing.preprocessor import IDestination

class IDestiny(ABC):
"""The basic element on which FileSaver operates."""

@property
@abstractmethod
def name(self) -> str:
"""The name of the file, including the extension. This will be used to save as the file name."""

@property
@abstractmethod
def content(self) -> str:
"""The contents of the file as test - it will be saved."""

@property
@abstractmethod
def path_prefix(self) -> Optional[Path]:
"""If set, it will be added to the root write path after which further path modifications are allowed."""


class FileSaver(IDestination):
"""Implementation that provides text file saving. It supports dynamically generated paths
(in real use, probably based on the file extension extracted from the name) and supports defined path prefixes."""

@dataclass
class Configuration:
"""Core FileSaver configuration"""
root_path: Path
"""Base path relative to which prefixes are added."""
path_prefix_generator: Optional[Callable[[IDestiny], Path]] = None
"""If it exists, the function is called for each IDestiny and the generated prefix is
appended after the fixed prefix from the IDestiny."""

def __init__(self, configuration: Configuration):
self._configuration = configuration

@property
def configuration(self) -> Configuration:
"""Current configuration."""
return self._configuration

@classmethod
def get_required_type(cls) -> Type:
return IDestiny

def consume(self, data: Iterable[IDestiny]) -> Optional[IDestination.Artifacts]:
result = IDestination.Artifacts()
for d in data:
root_path = self._configuration.root_path
if d.path_prefix is not None:
root_path /= d.path_prefix
if self._configuration.path_prefix_generator is not None:
root_path /= self._configuration.path_prefix_generator(d)
path = root_path / d.name
with open(root_path / d.name, "tw", encoding="utf-8") as f:
f.write(d.content)
result.files.append(path)
return result
4 changes: 2 additions & 2 deletions src/devana/preprocessing/preprocessor.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Optional, List, Type, Iterable, Any
from dataclasses import dataclass
from dataclasses import dataclass, field


class IInputContract(ABC):
Expand Down Expand Up @@ -40,7 +40,7 @@ class IDestination(IInputContract, ABC):
@dataclass
class Artifacts:
"""Resulting preprocessor artifacts."""
files: List[Path]
files: List[Path] = field(default_factory=list)
"""List of touching files."""

@abstractmethod
Expand Down
4 changes: 2 additions & 2 deletions src/devana/syntax_abstraction/organizers/sourcefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,12 @@ def type(self, value):

@property
def name(self) -> str:
"""Name of source file without extension."""
"""Name of a source file without extension."""
return self.path.name

@property
def extension(self) -> Union[Literal["h", "hpp", "hxx", "c", "cpp", "cxx", "cc"], str]:
"""File extension. In most common way, it will be standard, well know C++ extension."""
"""File extension. In the most common way, it will be standard, well know C++ extension."""
return self.path.suffix.lstrip(".")

@property
Expand Down
Empty file.
54 changes: 54 additions & 0 deletions tests/preprocessing/unit/components/savers/test_file_saver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import unittest
from dataclasses import dataclass
import dataclasses
from pathlib import Path
from typing import Optional
from unittest.mock import patch, mock_open
from devana.preprocessing.components.savers.file_saver import FileSaver, IDestiny


@dataclass
class TestDestiny(IDestiny):
name: str = dataclasses.MISSING
content: str = dataclasses.MISSING
path_prefix: Optional[Path] = dataclasses.MISSING


class TestFileSaver(unittest.TestCase):

@patch("builtins.open", new_callable=mock_open)
def test_basic_file_saver(self, mock_open_file):
saver = FileSaver(FileSaver.Configuration(Path("/test")))
destiny = TestDestiny("TestFileName.cpp", "Content tested", None)
result = saver.consume([destiny])
mock_open_file.assert_called_with(Path("/test/TestFileName.cpp"), "tw", encoding="utf-8")
self.assertEqual(len(result.files), 1)
self.assertEqual(result.files[0].name, "TestFileName.cpp")

@patch("builtins.open", new_callable=mock_open)
def test_add_prefix_for_file_saver(self, mock_open_file):
saver = FileSaver(FileSaver.Configuration(Path("/test")))
destiny = TestDestiny("TestFileName.cpp", "Content tested", Path("test_prefix"))
result = saver.consume([destiny])
mock_open_file.assert_called_with(Path("/test/test_prefix/TestFileName.cpp"), "tw", encoding="utf-8")
self.assertEqual(len(result.files), 1)
self.assertEqual(result.files[0].name, "TestFileName.cpp")


@patch("builtins.open", new_callable=mock_open)
def test_add_dynamic_prefix_for_file_saver(self, mock_open_file):
saver = FileSaver(FileSaver.Configuration(Path("/test"), lambda e: Path("dynamic_prefix")))
destiny = TestDestiny("TestFileName.cpp", "Content tested", None)
result = saver.consume([destiny])
mock_open_file.assert_called_with(Path("/test/dynamic_prefix/TestFileName.cpp"), "tw", encoding="utf-8")
self.assertEqual(len(result.files), 1)
self.assertEqual(result.files[0].name, "TestFileName.cpp")

@patch("builtins.open", new_callable=mock_open)
def test_add_dynamic_ans_static_prefix_for_file_saver(self, mock_open_file):
saver = FileSaver(FileSaver.Configuration(Path("/test"), lambda e: Path("dynamic_prefix")))
destiny = TestDestiny("TestFileName.cpp", "Content tested", Path("test_prefix"))
result = saver.consume([destiny])
mock_open_file.assert_called_with(Path("/test/test_prefix/dynamic_prefix/TestFileName.cpp"), "tw", encoding="utf-8")
self.assertEqual(len(result.files), 1)
self.assertEqual(result.files[0].name, "TestFileName.cpp")

0 comments on commit 6ad6f77

Please sign in to comment.