feat: RFC 135

This commit is contained in:
莘权 马 2023-11-22 17:08:00 +08:00
parent 27c731d11a
commit 2bf8ef8c6a
16 changed files with 416 additions and 82 deletions

View file

@ -11,8 +11,9 @@ from typing import List
from metagpt.actions import Action, ActionOutput
from metagpt.config import CONFIG
from metagpt.const import WORKSPACE_ROOT
from metagpt.const import PRDS_FILE_REPO, SYS_DESIGN_FILE_REPO, WORKSPACE_ROOT
from metagpt.logs import logger
from metagpt.schema import Document, Documents
from metagpt.utils.common import CodeParser
from metagpt.utils.get_template import get_template
from metagpt.utils.json_to_markdown import json_to_markdown
@ -202,7 +203,44 @@ class WriteDesign(Action):
await self._save_prd(docs_path, resources_path, context)
await self._save_system_design(docs_path, resources_path, system_design)
async def run(self, context, format=CONFIG.prompt_format):
async def run(self, with_messages, format=CONFIG.prompt_format):
# 通过git diff来识别docs/prds下哪些PRD文档发生了变动
prds_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO)
changed_prds = prds_file_repo.changed_files
# 通过git diff来识别docs/system_designs下那些设计文档发生了变动
system_design_file_repo = CONFIG.git_repo.new_file_repository(SYS_DESIGN_FILE_REPO)
changed_system_designs = system_design_file_repo.changed_files
# 对于那些发生变动的PRD和设计文档重新生成设计内容
changed_files = Documents()
for filename in changed_prds.keys():
prd = await prds_file_repo.get(filename)
old_system_design_doc = await system_design_file_repo.get(filename)
if not old_system_design_doc:
system_design = await self._run(context=prd.content)
doc = Document(
root_path=SYS_DESIGN_FILE_REPO, filename=filename, content=system_design.instruct_content.json()
)
else:
doc = await self._merge(prd_doc=prd, system_design_doc=old_system_design_doc)
await system_design_file_repo.save(
filename=filename, content=doc.content, dependencies={prd.root_relative_path}
)
changed_files.docs[filename] = doc
for filename in changed_system_designs.keys():
if filename in changed_files.docs:
continue
prd_doc = await prds_file_repo.get(filename=filename)
old_system_design_doc = await system_design_file_repo.get(filename)
new_system_design_doc = await self._merge(prd_doc, old_system_design_doc)
await system_design_file_repo.save(filename=filename, content=new_system_design_doc.content)
changed_files.docs[filename] = new_system_design_doc
# 等docs/system_designs/下所有文件都处理完才发publish message给后续做全局优化留空间。
return ActionOutput(content=changed_files.json(), instruct_content=changed_files)
async def _run(self, context, format=CONFIG.prompt_format):
prompt_template, format_example = get_template(templates, format)
prompt = prompt_template.format(context=context, format_example=format_example)
# system_design = await self._aask(prompt)
@ -213,5 +251,8 @@ class WriteDesign(Action):
"Python package name",
system_design.instruct_content.dict()["Python package name"].strip().strip("'").strip('"'),
)
await self._save(context, system_design)
# await self._save(context, system_design)
return system_design
async def _merge(self, prd_doc, system_design_doc):
return system_design_doc

View file

@ -7,19 +7,37 @@
@Desc: PrepareDocuments Action: initialize project folder and add new requirements to docs/requirements.txt.
RFC 135 2.2.3.5.1.
"""
from metagpt.actions import Action
from pathlib import Path
from metagpt.actions import Action, ActionOutput
from metagpt.config import CONFIG
from metagpt.const import DOCS_FILE_REPO, REQUIREMENT_FILENAME, WORKSPACE_ROOT
from metagpt.schema import Document
from metagpt.utils.file_repository import FileRepository
from metagpt.utils.git_repository import GitRepository
class PrepareDocuments(Action):
def __init__(self, name="", context=None, llm=None):
super().__init__(name, context, llm)
async def run(self, with_message, **kwargs):
parent = self.context.get("parent")
if not parent:
raise ValueError("Invalid owner")
env = parent.get_env()
if env.git_repository:
return
env.git_repository = GitRepository()
env.git_repository.open(WORKS)
async def run(self, with_messages, **kwargs):
if CONFIG.git_repo:
docs_repo = CONFIG.git_repo.new_file_repository(DOCS_FILE_REPO)
doc = await docs_repo.get(REQUIREMENT_FILENAME)
return ActionOutput(content=doc.json(exclue="content"), instruct_content=doc)
# Create and initialize the workspace folder, initialize the Git environment.
CONFIG.git_repo = GitRepository()
workdir = Path(CONFIG.WORKDIR) if CONFIG.WORKDIR else WORKSPACE_ROOT / FileRepository.new_file_name()
CONFIG.git_repo.open(local_path=workdir, auto_init=True)
# Write the newly added requirements from the main parameter idea to `docs/requirement.txt`.
docs_file_repository = CONFIG.git_repo.new_file_repository(DOCS_FILE_REPO)
doc = Document(root_path=DOCS_FILE_REPO, filename=REQUIREMENT_FILENAME, content=with_messages[0].content)
await docs_file_repository.save(REQUIREMENT_FILENAME, content=doc.content)
# Send a Message notification to the WritePRD action, instructing it to process requirements using
# `docs/requirement.txt` and `docs/prds/`.
return ActionOutput(content=doc.content, instruct_content=doc)

View file

@ -10,7 +10,10 @@ from typing import List
from metagpt.actions import Action, ActionOutput
from metagpt.actions.search_and_summarize import SearchAndSummarize
from metagpt.config import CONFIG
from metagpt.const import DOCS_FILE_REPO, PRDS_FILE_REPO, REQUIREMENT_FILENAME
from metagpt.logs import logger
from metagpt.schema import Document, Documents
from metagpt.utils.file_repository import FileRepository
from metagpt.utils.get_template import get_template
templates = {
@ -222,7 +225,34 @@ class WritePRD(Action):
def __init__(self, name="", context=None, llm=None):
super().__init__(name, context, llm)
async def run(self, requirements, format=CONFIG.prompt_format, *args, **kwargs) -> ActionOutput:
async def run(self, with_messages, format=CONFIG.prompt_format, *args, **kwargs) -> ActionOutput:
# 判断哪些需求文档需要重写调LLM判断新增需求与prd是否相关若相关就rewrite prd
docs_file_repo = CONFIG.git_repo.new_file_repository(DOCS_FILE_REPO)
requirement_doc = await docs_file_repo.get(REQUIREMENT_FILENAME)
prds_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO)
prd_docs = await prds_file_repo.get_all()
change_files = Documents()
for prd_doc in prd_docs:
if await self._is_relative_to(requirement_doc, prd_doc):
prd_doc = await self._merge(requirement_doc, prd_doc)
await prds_file_repo.save(filename=prd_doc.filename, content=prd_doc.content)
change_files.docs[prd_doc.filename] = prd_doc
# 如果没有任何PRD就使用docs/requirement.txt生成一个prd
if not change_files.docs:
prd = await self._run_new_requirement(
requirements=[requirement_doc.content], format=format, *args, **kwargs
)
doc = Document(
root_path=PRDS_FILE_REPO,
filename=FileRepository.new_file_name() + ".json",
content=prd.instruct_content.json(),
)
await prds_file_repo.save(filename=doc.filename, content=doc.content)
change_files.docs[doc.filename] = doc
# 等docs/prds/下所有文件都与新增需求对比完后再触发publish message让工作流跳转到下一环节。如此设计是为了给后续做全局优化留空间。
return ActionOutput(content=change_files.json(), instruct_content=change_files)
async def _run_new_requirement(self, requirements, format=CONFIG.prompt_format, *args, **kwargs) -> ActionOutput:
sas = SearchAndSummarize()
# rsp = await sas.run(context=requirements, system_text=SEARCH_AND_SUMMARIZE_SYSTEM_EN_US)
rsp = ""
@ -239,3 +269,9 @@ class WritePRD(Action):
# prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING)
prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING, format=format)
return prd
async def _is_relative_to(self, doc1, doc2) -> bool:
return False
async def _merge(self, doc1, doc2) -> Document:
pass

View file

@ -46,7 +46,7 @@ class Config(metaclass=Singleton):
self.openai_api_key = self._get("OPENAI_API_KEY")
self.anthropic_api_key = self._get("Anthropic_API_KEY")
if (not self.openai_api_key or "YOUR_API_KEY" == self.openai_api_key) and (
not self.anthropic_api_key or "YOUR_API_KEY" == self.anthropic_api_key
not self.anthropic_api_key or "YOUR_API_KEY" == self.anthropic_api_key
):
raise NotConfiguredException("Set OPENAI_API_KEY or Anthropic_API_KEY first")
self.openai_api_base = self._get("OPENAI_API_BASE")
@ -93,6 +93,7 @@ class Config(metaclass=Singleton):
self.pyppeteer_executable_path = self._get("PYPPETEER_EXECUTABLE_PATH", "")
self.prompt_format = self._get("PROMPT_FORMAT", "markdown")
self.git_repo = None
def _init_with_config_files_and_env(self, configs: dict, yaml_file):
"""Load from config/key.yaml, config/config.yaml, and env in decreasing order of priority"""

View file

@ -49,3 +49,8 @@ MESSAGE_ROUTE_TO = "send_to"
MESSAGE_ROUTE_CAUSE_BY = "cause_by"
MESSAGE_META_ROLE = "role"
MESSAGE_ROUTE_TO_ALL = "<all>"
REQUIREMENT_FILENAME = "requirement.txt"
DOCS_FILE_REPO = "docs"
PRDS_FILE_REPO = "docs/prds"
SYS_DESIGN_FILE_REPO = "docs/system_design"

View file

@ -12,7 +12,7 @@
functionality is to be consolidated into the `Environment` class.
"""
import asyncio
from typing import Iterable, Optional, Set
from typing import Iterable, Set
from pydantic import BaseModel, Field
@ -20,7 +20,6 @@ from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.utils.common import is_subscribed
from metagpt.utils.git_repository import GitRepository
class Environment(BaseModel):
@ -32,7 +31,6 @@ class Environment(BaseModel):
roles: dict[str, Role] = Field(default_factory=dict)
consumers: dict[Role, Set] = Field(default_factory=dict)
history: str = Field(default="") # For debug
git_repository: Optional[GitRepository] = None
class Config:
arbitrary_types_allowed = True
@ -113,8 +111,3 @@ class Environment(BaseModel):
def set_subscription(self, obj, tags):
"""Set the labels for message to be consumed by the object"""
self.consumers[obj] = tags
def dict(self, *args, **kwargs):
"""Generate a dictionary representation of the model, optionally specifying which fields to include or
exclude."""
return super(Environment, self).dict(exclude={"git_repository"})

View file

@ -7,6 +7,7 @@
"""
from metagpt.actions import BossRequirement, WritePRD
from metagpt.actions.prepare_documents import PrepareDocuments
from metagpt.config import CONFIG
from metagpt.roles import Role
@ -38,12 +39,12 @@ class ProductManager(Role):
constraints (str): Constraints or limitations for the product manager.
"""
super().__init__(name, profile, goal, constraints)
self._init_actions([PrepareDocuments(context={"parent": self}), WritePRD])
self._watch([BossRequirement])
self._init_actions([PrepareDocuments, WritePRD])
self._watch([BossRequirement, PrepareDocuments])
async def _think(self) -> None:
"""Decide what to do"""
if self._rc.env.git_repository:
if CONFIG.git_repo:
self._set_state(1)
else:
self._set_state(0)

View file

@ -6,14 +6,16 @@
@File : schema.py
@Modified By: mashenquan, 2023-10-31. According to Chapter 2.2.1 of RFC 116:
Replanned the distribution of responsibilities and functional positioning of `Message` class attributes.
@Modified By: mashenquan, 2023/11/22. Add `Document` and `Documents` for `FileRepository` in Section 2.2.3.4 of RFC 135.
"""
from __future__ import annotations
import asyncio
import json
import os.path
from asyncio import Queue, QueueEmpty, wait_for
from json import JSONDecodeError
from typing import List, Set, TypedDict
from typing import Dict, List, Optional, Set, TypedDict
from pydantic import BaseModel, Field
@ -32,6 +34,42 @@ class RawMessage(TypedDict):
role: str
class Document(BaseModel):
"""
Represents a document.
"""
root_path: str
filename: str
content: Optional[str] = None
def get_meta(self) -> Document:
"""Get metadata of the document.
:return: A new Document instance with the same root path and filename.
"""
return Document(root_path=self.root_path, filename=self.filename)
@property
def root_relative_path(self):
"""Get relative path from root of git repository.
:return: relative path from root of git repository.
"""
return os.path.join(self.root_path, self.filename)
class Documents(BaseModel):
"""A class representing a collection of documents.
Attributes:
docs (Dict[str, Document]): A dictionary mapping document names to Document instances.
"""
docs: Dict[str, Document] = Field(default_factory=dict)
class Message(BaseModel):
"""list[<role>: <content>]"""

View file

@ -0,0 +1,83 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2023/11/22
@Author : mashenquan
@File : dependency_file.py
@Desc: Implementation of the dependency file described in Section 2.2.3.2 of RFC 135.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Set
import aiofiles
from metagpt.logs import logger
class DependencyFile:
def __init__(self, workdir: Path | str):
self._dependencies = {}
self._filename = Path(workdir) / ".dependencies.json"
async def load(self):
if not self._filename.exists():
return
try:
async with aiofiles.open(str(self._filename), mode="r") as reader:
data = await reader.read()
self._dependencies = json.loads(data)
except Exception as e:
logger.error(f"Failed to load {str(self._filename)}, error:{e}")
async def save(self):
try:
data = json.dumps(self._dependencies)
async with aiofiles.open(str(self._filename), mode="w") as writer:
await writer.write(data)
except Exception as e:
logger.error(f"Failed to save {str(self._filename)}, error:{e}")
async def update(self, filename: Path | str, dependencies: Set[Path | str], persist=True):
if persist:
await self.load()
root = self._filename.parent
try:
key = Path(filename).relative_to(root)
except ValueError:
key = filename
if dependencies:
relative_paths = []
for i in dependencies:
try:
relative_paths.append(str(Path(i).relative_to(root)))
except ValueError:
relative_paths.append(str(i))
self._dependencies[str(key)] = relative_paths
elif str(key) in self._dependencies:
del self._dependencies[str(key)]
if persist:
await self.save()
async def get(self, filename: Path | str, persist=False):
if persist:
await self.load()
root = self._filename.parent
try:
key = Path(filename).relative_to(root)
except ValueError:
key = filename
return set(self._dependencies.get(str(key), {}))
def delete_file(self):
self._filename.unlink(missing_ok=True)
@property
def exists(self):
return self._filename.exists()

View file

@ -8,16 +8,29 @@
"""
from __future__ import annotations
import json
import os
import uuid
from datetime import datetime
from pathlib import Path
from typing import Dict, List
from typing import Dict, List, Set
import aiofiles
from metagpt.logs import logger
from metagpt.schema import Document
class FileRepository:
"""A class representing a FileRepository associated with a Git repository.
:param git_repo: The associated GitRepository instance.
:param relative_path: The relative path within the Git repository.
Attributes:
_relative_path (Path): The relative path within the Git repository.
_git_repo (GitRepository): The associated GitRepository instance.
"""
def __init__(self, git_repo, relative_path: Path = Path(".")):
"""Initialize a FileRepository instance.
@ -26,16 +39,9 @@ class FileRepository:
"""
self._relative_path = relative_path
self._git_repo = git_repo
self._dependencies: Dict[str, List[str]] = {}
# Initializing
self.workdir.mkdir(parents=True, exist_ok=True)
if self.dependency_path_name.exists():
try:
with open(str(self.dependency_path_name), mode="r") as reader:
self._dependencies = json.load(reader)
except Exception as e:
logger.error(f"Failed to load {str(self.dependency_path_name)}, error:{e}")
async def save(self, filename: Path | str, content, dependencies: List[str] = None):
"""Save content to a file and update its dependencies.
@ -44,59 +50,68 @@ class FileRepository:
:param content: The content to be saved.
:param dependencies: List of dependency filenames or paths.
"""
path_name = self.workdir / filename
path_name.parent.mkdir(parents=True, exist_ok=True)
async with aiofiles.open(str(path_name), mode="w") as writer:
pathname = self.workdir / filename
pathname.parent.mkdir(parents=True, exist_ok=True)
async with aiofiles.open(str(pathname), mode="w") as writer:
await writer.write(content)
logger.info(f"save to: {str(pathname)}")
if dependencies is not None:
await self.update_dependency(filename, dependencies)
dependency_file = await self._git_repo.get_dependency()
await dependency_file.update(pathname, set(dependencies))
logger.info(f"update dependency: {str(pathname)}:{dependencies}")
async def get(self, filename: Path | str):
"""Read the content of a file.
:param filename: The filename or path within the repository.
:return: The content of the file.
"""
path_name = self.workdir / filename
async with aiofiles.open(str(path_name), mode="r") as reader:
return await reader.read()
def get_dependency(self, filename: Path | str) -> List:
async def get_dependency(self, filename: Path | str) -> Set[str]:
"""Get the dependencies of a file.
:param filename: The filename or path within the repository.
:return: List of dependency filenames or paths.
:return: Set of dependency filenames or paths.
"""
key = str(filename)
return self._dependencies.get(key, [])
pathname = self.workdir / filename
dependency_file = await self._git_repo.get_dependency()
return await dependency_file.get(pathname)
def get_changed_dependency(self, filename: Path | str) -> List:
async def get_changed_dependency(self, filename: Path | str) -> Set[str]:
"""Get the dependencies of a file that have changed.
:param filename: The filename or path within the repository.
:return: List of changed dependency filenames or paths.
"""
dependencies = self.get_dependency(filename=filename)
dependencies = await self.get_dependency(filename=filename)
changed_files = self.changed_files
changed_dependent_files = []
changed_dependent_files = set()
for df in dependencies:
if df in changed_files.keys():
changed_dependent_files.append(df)
changed_dependent_files.add(df)
return changed_dependent_files
async def update_dependency(self, filename, dependencies: List[str]):
"""Update the dependencies of a file.
async def get(self, filename: Path | str) -> Document | None:
"""Read the content of a file.
:param filename: The filename or path within the repository.
:param dependencies: List of dependency filenames or paths.
:return: The content of the file.
"""
self._dependencies[str(filename)] = dependencies
doc = Document(root_path=str(self.root_path), filename=str(filename))
path_name = self.workdir / filename
if not path_name.exists():
return None
async with aiofiles.open(str(path_name), mode="r") as reader:
doc.content = await reader.read()
return doc
async def save_dependency(self):
"""Save the dependencies to a file."""
data = json.dumps(self._dependencies)
with aiofiles.open(str(self.dependency_path_name), mode="w") as writer:
await writer.write(data)
async def get_all(self) -> List[Document]:
"""Get the content of all files in the repository.
:return: List of Document instances representing files.
"""
docs = []
for root, dirs, files in os.walk(str(self.workdir)):
for file in files:
file_path = Path(root) / file
relative_path = file_path.relative_to(self.workdir)
doc = await self.get(relative_path)
docs.append(doc)
return docs
@property
def workdir(self):
@ -107,14 +122,9 @@ class FileRepository:
return self._git_repo.workdir / self._relative_path
@property
def dependency_path_name(self):
"""Return the absolute path to the dependency file.
:return: The absolute path to the dependency file.
"""
filename = ".dependencies.json"
path_name = self.workdir / filename
return path_name
def root_path(self):
"""Return the relative path from git repository root"""
return self._relative_path
@property
def changed_files(self) -> Dict[str, str]:
@ -147,3 +157,13 @@ class FileRepository:
continue
children.append(str(f))
return children
@staticmethod
def new_file_name():
"""Generate a new filename based on the current timestamp and a UUID suffix.
:return: A new filename string.
"""
current_time = datetime.now().strftime("%Y%m%d%H%M%S")
guid_suffix = str(uuid.uuid4())[:8]
return f"{current_time}t{guid_suffix}"

View file

@ -17,6 +17,7 @@ from git.repo import Repo
from git.repo.fun import is_git_dir
from metagpt.const import WORKSPACE_ROOT
from metagpt.utils.dependency_file import DependencyFile
from metagpt.utils.file_repository import FileRepository
@ -47,6 +48,7 @@ class GitRepository:
:param auto_init: If True, automatically initializes a new Git repository if the provided path is not a Git repository.
"""
self._repository = None
self._dependency = None
if local_path:
self.open(local_path=local_path, auto_init=auto_init)
@ -113,7 +115,7 @@ class GitRepository:
:param local_path: The local path to check.
:return: True if the directory is a Git repository, False otherwise.
"""
git_dir = local_path / ".git"
git_dir = Path(local_path) / ".git"
if git_dir.exists() and is_git_dir(git_dir):
return True
return False
@ -151,7 +153,7 @@ class GitRepository:
self.add_change(self.changed_files)
self.commit(comments)
def new_file_repository(self, relative_path: Path | str) -> FileRepository:
def new_file_repository(self, relative_path: Path | str = ".") -> FileRepository:
"""Create a new instance of FileRepository associated with this Git repository.
:param relative_path: The relative path to the file repository within the Git repository.
@ -159,6 +161,15 @@ class GitRepository:
"""
return FileRepository(git_repo=self, relative_path=Path(relative_path))
async def get_dependency(self) -> DependencyFile:
"""Get the dependency file associated with the Git repository.
:return: An instance of DependencyFile.
"""
if not self._dependency:
self._dependency = DependencyFile(workdir=self.workdir)
return self._dependency
if __name__ == "__main__":
path = WORKSPACE_ROOT / "git"

View file

@ -44,4 +44,4 @@ ta==0.10.2
semantic-kernel==0.3.13.dev0
wrapt==1.15.0
websocket-client==0.58.0
aiofiles==23.2.1

View file

@ -4,6 +4,7 @@ import asyncio
import fire
from metagpt.config import CONFIG
from metagpt.roles import (
Architect,
Engineer,
@ -54,6 +55,7 @@ def main(
code_review: bool = True,
run_tests: bool = False,
implement: bool = True,
project_path: str = None,
):
"""
We are a software startup comprised of AI. By investing in us,
@ -63,8 +65,12 @@ def main(
a certain dollar amount to this AI company.
:param n_round:
:param code_review: Whether to use code review.
:param run_tests: Whether run unit tests.
:param implement: Whether to write codes.
:param project_path: The path of the old version project to improve.
:return:
"""
CONFIG.WORKDIR = project_path
asyncio.run(startup(idea, investment, n_round, code_review, run_tests, implement))

View file

@ -0,0 +1,64 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2023/11/22
@Author : mashenquan
@File : test_dependency_file.py
@Desc: Unit tests for dependency_file.py
"""
from __future__ import annotations
from pathlib import Path
from typing import Optional, Set, Union
import pytest
from pydantic import BaseModel
from metagpt.utils.dependency_file import DependencyFile
@pytest.mark.asyncio
async def test_dependency_file():
class Input(BaseModel):
x: Union[Path, str]
deps: Optional[Set[Union[Path, str]]]
key: Optional[Union[Path, str]]
want: Set[str]
inputs = [
Input(x="a/b.txt", deps={"c/e.txt", Path(__file__).parent / "d.txt"}, want={"c/e.txt", "d.txt"}),
Input(
x=Path(__file__).parent / "x/b.txt",
deps={"s/e.txt", Path(__file__).parent / "d.txt"},
key="x/b.txt",
want={"s/e.txt", "d.txt"},
),
Input(x="f.txt", deps=None, want=set()),
Input(x="a/b.txt", deps=None, want=set()),
]
file = DependencyFile(workdir=Path(__file__).parent)
for i in inputs:
await file.update(filename=i.x, dependencies=i.deps)
assert await file.get(filename=i.key or i.x) == i.want
file2 = DependencyFile(workdir=Path(__file__).parent)
file2.delete_file()
assert not file.exists
await file2.update(filename="a/b.txt", dependencies={"c/e.txt", Path(__file__).parent / "d.txt"}, persist=False)
assert not file.exists
await file2.save()
assert file2.exists
file1 = DependencyFile(workdir=Path(__file__).parent)
assert file1.exists
assert await file1.get("a/b.txt") == set()
await file1.load()
assert await file1.get("a/b.txt") == {"c/e.txt", "d.txt"}
file1.delete_file()
assert not file.exists
if __name__ == "__main__":
pytest.main([__file__, "-s"])

View file

@ -34,11 +34,13 @@ async def test_file_repo():
assert file_repo.workdir.exists()
await file_repo.save("a.txt", "AAA")
await file_repo.save("b.txt", "BBB", ["a.txt"])
assert "AAA" == await file_repo.get("a.txt")
assert "BBB" == await file_repo.get("b.txt")
assert ["a.txt"] == file_repo.get_dependency("b.txt")
doc = await file_repo.get("a.txt")
assert "AAA" == doc.content
doc = await file_repo.get("b.txt")
assert "BBB" == doc.content
assert {"a.txt"} == await file_repo.get_dependency("b.txt")
assert {"a.txt": ChangeType.UNTRACTED, "b.txt": ChangeType.UNTRACTED} == file_repo.changed_files
assert ["a.txt"] == file_repo.get_changed_dependency("b.txt")
assert {"a.txt"} == await file_repo.get_changed_dependency("b.txt")
await file_repo.save("d/e.txt", "EEE")
assert ["d/e.txt"] == file_repo.get_change_dir_files("d")

View file

@ -77,5 +77,20 @@ async def test_git1():
assert not local_path.exists()
@pytest.mark.asyncio
async def test_dependency_file():
local_path = Path(__file__).parent / "git2"
repo, subdir = await mock_repo(local_path)
dependancy_file = await repo.get_dependency()
assert not dependancy_file.exists
await dependancy_file.update(filename="a/b.txt", dependencies={"c/d.txt", "e/f.txt"})
assert dependancy_file.exists
repo.delete_repository()
assert not dependancy_file.exists
if __name__ == "__main__":
pytest.main([__file__, "-s"])