Refactor writing/formatting systems
This commit is contained in:
@@ -1,16 +1,13 @@
|
||||
from mcpacker.write.markdown.markdownwriter import MarkdownWriter
|
||||
from mcpacker.model.modpack import ModPack
|
||||
from mcpacker.format.report.composer import ReportComposer
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class BiomeReport(MarkdownWriter):
|
||||
class BiomeReport(ReportComposer):
|
||||
|
||||
def __init__(self, pack:ModPack, outputDir:Path):
|
||||
super().__init__("biomes.md", pack, outputDir)
|
||||
|
||||
def compose(self):
|
||||
def doCompose(self):
|
||||
for biome in self.pack.world.biomes:
|
||||
self.line(f"# Biome: {biome.gameId} ({biome.city})")
|
||||
self.line()
|
||||
+10
-11
@@ -1,7 +1,9 @@
|
||||
from mcpacker.write.markdown.biomereport import BiomeReport
|
||||
from mcpacker.format.report.biomereport import BiomeReport
|
||||
from mcpacker.model.core.ecology.biome import Biome
|
||||
from mcpacker.model.modpack import AugmentFunc
|
||||
from mcpacker.model.modpack import ModPack
|
||||
from pytest import fixture
|
||||
from typing import Callable
|
||||
|
||||
import mcpacker.model.core.ecology.flora as F
|
||||
import mcpacker.model.core.ecology.geology as G
|
||||
@@ -32,22 +34,20 @@ def defineAddBiomes():
|
||||
return addBiomes
|
||||
|
||||
@fixture(name="pack")
|
||||
def createPack(addBiomes):
|
||||
def createPack(addBiomes:AugmentFunc):
|
||||
pack = ModPack("testModPack")
|
||||
pack.augment(addBiomes)
|
||||
yield pack
|
||||
|
||||
@fixture(name="report")
|
||||
def createReport(pack, tmp_path):
|
||||
report = BiomeReport(pack, tmp_path)
|
||||
report.write()
|
||||
yield report
|
||||
def createReport(pack:ModPack):
|
||||
yield BiomeReport(pack).compose()
|
||||
|
||||
|
||||
# Tests ############################################################################################
|
||||
|
||||
def test_report(tmp_path, report):
|
||||
path = tmp_path / "testModPack" / "reports" / "biomes.md"
|
||||
assert path.read_text() == textwrap.dedent("""
|
||||
def test_report(report):
|
||||
assert str(report).strip() == textwrap.dedent("""
|
||||
# Biome: minecraft:plains (kansascity)
|
||||
|
||||
* Flora: field
|
||||
@@ -65,5 +65,4 @@ def test_report(tmp_path, report):
|
||||
* Humidity: dry
|
||||
* Soil: sandy
|
||||
* Water: inland
|
||||
|
||||
""").lstrip()
|
||||
""").strip()
|
||||
@@ -0,0 +1,11 @@
|
||||
from mcpacker.format.textcomposer import TextComposer
|
||||
from mcpacker.model.modpack import ModPack
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class ReportComposer(TextComposer):
|
||||
|
||||
def __init__(self, pack:ModPack, indentText:str=" "):
|
||||
super().__init__(indentText)
|
||||
self.pack = pack
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import mcpacker.write.markdown.reportwriter
|
||||
import mcpacker.format.report.composer
|
||||
|
||||
|
||||
# Tests ############################################################################################
|
||||
@@ -1,16 +1,13 @@
|
||||
from mcpacker.write.markdown.markdownwriter import MarkdownWriter
|
||||
from mcpacker.format.report.composer import ReportComposer
|
||||
from mcpacker.model.modpack import ModPack
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class MineralReport(MarkdownWriter):
|
||||
class MineralReport(ReportComposer):
|
||||
|
||||
def __init__(self, pack:ModPack, outputDir:Path):
|
||||
super().__init__("minerals.md", pack, outputDir)
|
||||
|
||||
def compose(self):
|
||||
def doCompose(self):
|
||||
for mineral in self.pack.world.minerals:
|
||||
self.line(f"# Mineral: {mineral.name}")
|
||||
self.line()
|
||||
+9
-15
@@ -1,6 +1,7 @@
|
||||
from mcpacker.write.markdown.mineralreport import MineralReport
|
||||
from mcpacker.format.report.mineralreport import MineralReport
|
||||
from mcpacker.model.core.geology.mineral import Mineral
|
||||
from mcpacker.model.core.geology.replacement import Replacement
|
||||
from mcpacker.model.modpack import AugmentFunc
|
||||
from mcpacker.model.modpack import ModPack
|
||||
from pytest import fixture
|
||||
|
||||
@@ -25,24 +26,18 @@ def defineAddMinerals():
|
||||
return addMinerals
|
||||
|
||||
@fixture(name="pack")
|
||||
def createPack(addMinerals):
|
||||
pack = ModPack("testModPack")
|
||||
pack.augment(addMinerals)
|
||||
yield pack
|
||||
def createPack(addMinerals:AugmentFunc):
|
||||
yield ModPack("testModPack").augment(addMinerals)
|
||||
|
||||
@fixture(name="report")
|
||||
def createReport(pack, tmp_path):
|
||||
report = MineralReport(pack, tmp_path)
|
||||
report.write()
|
||||
yield report
|
||||
def createReport(pack:ModPack):
|
||||
yield MineralReport(pack).compose()
|
||||
|
||||
|
||||
# Tests ############################################################################################
|
||||
|
||||
def test_report(tmp_path, report):
|
||||
path = tmp_path / "testModPack" / "reports" / "minerals.md"
|
||||
|
||||
assert path.read_text() == textwrap.dedent("""
|
||||
def test_report(report:MineralReport):
|
||||
assert str(report).strip() == textwrap.dedent("""
|
||||
# Mineral: copper
|
||||
|
||||
* #minecraft:stone_replaceables => minecraft:copper_ore
|
||||
@@ -52,5 +47,4 @@ def test_report(tmp_path, report):
|
||||
|
||||
* #minecraft:stone_replaceables => minecraft:iron_ore
|
||||
* #minecraft:deepslate_replaceables => minecraft:deepslate_iron_ore
|
||||
|
||||
""").lstrip()
|
||||
""").strip()
|
||||
+3
-6
@@ -1,17 +1,14 @@
|
||||
from mcpacker.format.report.composer import ReportComposer
|
||||
from mcpacker.model.core.ecology.biometrait import BiomeTrait
|
||||
from mcpacker.model.modpack import ModPack
|
||||
from mcpacker.write.markdown.markdownwriter import MarkdownWriter
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class MobSpawnReport(MarkdownWriter):
|
||||
class MobSpawnReport(ReportComposer):
|
||||
|
||||
def __init__(self, pack:ModPack, outputDir:Path):
|
||||
super().__init__("mobspawns.md", pack, outputDir)
|
||||
|
||||
def compose(self):
|
||||
def doCompose(self):
|
||||
for spawn in self.pack.world.mobSpawns:
|
||||
self.line(f"# Mob: {spawn.gameId}")
|
||||
self.line()
|
||||
+10
-15
@@ -1,11 +1,12 @@
|
||||
from mcpacker.format.report.mobspawnreport import MobSpawnReport
|
||||
from mcpacker.model.core.ecology.biomefilter import BiomeFilter as BF
|
||||
from mcpacker.model.core.fauna.mob import Mob
|
||||
from mcpacker.model.core.fauna.mobspawn import MobSpawn
|
||||
from mcpacker.model.core.geology.mineral import Mineral
|
||||
from mcpacker.model.core.geology.replacement import Replacement
|
||||
from mcpacker.model.core.habitat import Habitat
|
||||
from mcpacker.model.modpack import AugmentFunc
|
||||
from mcpacker.model.modpack import ModPack
|
||||
from mcpacker.write.markdown.mobspawnreport import MobSpawnReport
|
||||
from pytest import fixture
|
||||
|
||||
import mcpacker.model.core.altitude as AL
|
||||
@@ -56,23 +57,18 @@ def defineAddMobSpawnss():
|
||||
yield addMobSpawns
|
||||
|
||||
@fixture(name="pack")
|
||||
def createPack(addMobs, addMobSpawns):
|
||||
pack = ModPack("testModPack")
|
||||
pack.augment(addMobs)
|
||||
pack.augment(addMobSpawns)
|
||||
yield pack
|
||||
def createPack(addMobs:AugmentFunc, addMobSpawns:AugmentFunc):
|
||||
yield ModPack("testModPack").augment(addMobs).augment(addMobSpawns)
|
||||
|
||||
@fixture(name="report")
|
||||
def createReport(pack, tmp_path):
|
||||
report = MobSpawnReport(pack, tmp_path)
|
||||
report.write()
|
||||
yield report
|
||||
def createReport(pack:ModPack):
|
||||
yield MobSpawnReport(pack).compose()
|
||||
|
||||
|
||||
# Tests ############################################################################################
|
||||
|
||||
def test_write(report, tmp_path):
|
||||
path = tmp_path / "testModPack" / "reports" / "mobspawns.md"
|
||||
assert path.read_text() == textwrap.dedent("""
|
||||
def test_write(report:MobSpawnReport):
|
||||
assert str(report).strip() == textwrap.dedent("""
|
||||
# Mob: minecraft:chicken
|
||||
|
||||
* habitat 1
|
||||
@@ -95,5 +91,4 @@ def test_write(report, tmp_path):
|
||||
* group: troup
|
||||
* location: outside
|
||||
* scarcity: uncommon
|
||||
|
||||
""").lstrip()
|
||||
""").strip()
|
||||
@@ -0,0 +1,63 @@
|
||||
from typing import Any
|
||||
from typing import Self
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class TextComposer:
|
||||
|
||||
def __init__(self, indentText:str=" "):
|
||||
self._indent = 0
|
||||
self._indentText = indentText
|
||||
self._lineBuffer:list[str] = []
|
||||
self._lines:list[str] = []
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "\n".join(self._lines)
|
||||
|
||||
def compose(self) -> Self:
|
||||
self.doCompose()
|
||||
return self
|
||||
|
||||
# Helper Methods ###########################################################
|
||||
|
||||
def doCompose(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def indent(self) -> Self:
|
||||
self._indent += 1
|
||||
self._lineBuffer.insert(0, self._indentText)
|
||||
return self
|
||||
|
||||
def line(self, text:Any="") -> Self:
|
||||
self.text(text)
|
||||
|
||||
self._lines.append("".join(self._lineBuffer))
|
||||
self._lineBuffer = []
|
||||
while len(self._lineBuffer) < self._indent:
|
||||
self._lineBuffer.append(self._indentText)
|
||||
|
||||
return self
|
||||
|
||||
def outdent(self) -> Self:
|
||||
self._indent = max(0, self._indent - 1)
|
||||
|
||||
if self._lineBuffer:
|
||||
if self._lineBuffer[0] == self._indentText:
|
||||
del self._lineBuffer[0]
|
||||
|
||||
return self
|
||||
|
||||
def reset(self) -> Self:
|
||||
self._lineBuffer = []
|
||||
self._lines = []
|
||||
self._indent = 0
|
||||
return self
|
||||
|
||||
def resetIndent(self) -> Self:
|
||||
self._indent = 0
|
||||
return self
|
||||
|
||||
def text(self, text:Any) -> Self:
|
||||
self._lineBuffer.append(str(text))
|
||||
return self
|
||||
@@ -0,0 +1,31 @@
|
||||
from mcpacker.format.textcomposer import TextComposer
|
||||
from pytest import fixture
|
||||
|
||||
import textwrap
|
||||
|
||||
|
||||
# Helper Classes ###################################################################################
|
||||
|
||||
class SampleComposer(TextComposer):
|
||||
|
||||
def doCompose(self):
|
||||
self.text("alpha").line(" bravo")
|
||||
self.indent()
|
||||
self.line("charlie")
|
||||
self.outdent()
|
||||
self.text("delta").line(" echo")
|
||||
|
||||
# Fixtures #########################################################################################
|
||||
|
||||
@fixture(name="composer")
|
||||
def composeText():
|
||||
yield SampleComposer().compose()
|
||||
|
||||
# Tests ############################################################################################
|
||||
|
||||
def test_composer(composer:TextComposer):
|
||||
assert str(composer) == textwrap.dedent("""
|
||||
alpha bravo
|
||||
charlie
|
||||
delta echo
|
||||
""").strip()
|
||||
@@ -0,0 +1,254 @@
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
|
||||
# Helper Classes ###################################################################################
|
||||
|
||||
class Token:
|
||||
|
||||
def __init__(self, name:str, text:str, line:int):
|
||||
self.name = name
|
||||
self.line = line
|
||||
self.text = text
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"Token{{" +
|
||||
f"name: {self.name}, " +
|
||||
f"text: \"{self.text}\", " +
|
||||
f"line: {self.line}" +
|
||||
"}}"
|
||||
)
|
||||
|
||||
class TokenizationError(Exception):
|
||||
|
||||
def __init__(self, message:str, line:int):
|
||||
self.message = message
|
||||
self.line = line
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"TokenizationError:{self.line}: {self.message}"
|
||||
|
||||
|
||||
# Constants ########################################################################################
|
||||
|
||||
DIGIT_SET = "0123456789"
|
||||
IDENTIFIER_SET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
|
||||
SIGN_SET = "-+"
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class Tokenizer:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text:str,
|
||||
skipWhitespace:bool=True,
|
||||
comment:str="#",
|
||||
escape:str="\\",
|
||||
quotes:str="'\"",
|
||||
):
|
||||
self._comment = comment
|
||||
self._current = 0
|
||||
self._line = 0
|
||||
self._escape = escape
|
||||
self._quotes = quotes
|
||||
self._returnedTokens:list[Token] = []
|
||||
self._skipWhitespace = skipWhitespace
|
||||
self._text = text
|
||||
|
||||
def all(self) -> Iterable[Token]:
|
||||
result = []
|
||||
while self.peek().name != "eof":
|
||||
result.append(self.next())
|
||||
|
||||
return result
|
||||
|
||||
def isFinished(self) -> bool:
|
||||
return self._current >= len(self._text)
|
||||
|
||||
def next(self) -> Token:
|
||||
while True:
|
||||
if self._returnedTokens:
|
||||
token = self._returnedTokens.pop()
|
||||
return token
|
||||
|
||||
if self.isFinished():
|
||||
token = Token("eof", "", self._line)
|
||||
return token
|
||||
|
||||
token = (
|
||||
# order matters here: some tokens are subsets of other tokens, and must appear
|
||||
# later in the list to avoid mistakes (e.g., bool/indentifier, float/int)
|
||||
self._scanLetterSequence("open-paren", "(") or
|
||||
self._scanLetterSequence("close-paren", ")") or
|
||||
self._scanLetterSequence("open-brace", "{") or
|
||||
self._scanLetterSequence("close-brace", "}") or
|
||||
self._scanLetterSequence("open-bracket", "[") or
|
||||
self._scanLetterSequence("close-bracket", "]") or
|
||||
self._scanLetterSequence("period", ".") or
|
||||
self._scanLetterSequence("comma", ",") or
|
||||
self._scanLetterSequence("equal", "=") or
|
||||
self._scanLetterSequence("colon", ":") or
|
||||
self._scanLetterSequence("bool", "false") or
|
||||
self._scanLetterSequence("bool", "true") or
|
||||
self._scanLetterSet("whitespace", " \t") or
|
||||
self._scanLetterSet("newline", "\r\n") or
|
||||
self._scanFloat() or
|
||||
self._scanBase10Int() or
|
||||
self._scanIdentifier() or
|
||||
self._scanQuotedText() or
|
||||
self._scanLineComment() or
|
||||
self._scanOther()
|
||||
)
|
||||
|
||||
if (token.name in ["whitespace", "newline"]) and self._skipWhitespace:
|
||||
continue
|
||||
|
||||
return token
|
||||
|
||||
def peek(self) -> Token:
|
||||
token = self.next()
|
||||
self.reject(token)
|
||||
|
||||
return token
|
||||
|
||||
def reject(self, token:Token):
|
||||
self._returnedTokens.append(token)
|
||||
|
||||
# Private Methods ##########################################################
|
||||
|
||||
def _currentChar(self) -> str:
|
||||
if self._current >= len(self._text): return ""
|
||||
return self._text[self._current]
|
||||
|
||||
def _nextLetter(self) -> int:
|
||||
if self._current < len(self._text):
|
||||
if self._currentChar() == "\n":
|
||||
self._line += 1
|
||||
self._current += 1
|
||||
|
||||
return self._current
|
||||
|
||||
def _scanBase10Int(self) -> Token|None:
|
||||
start = self._current
|
||||
char = self._currentChar()
|
||||
if char in SIGN_SET:
|
||||
self._nextLetter() # skip sign
|
||||
char = self._currentChar()
|
||||
|
||||
if char not in DIGIT_SET: return None
|
||||
|
||||
while self._currentChar() in DIGIT_SET:
|
||||
self._nextLetter()
|
||||
|
||||
return Token("int", self._text[start:self._current], self._line)
|
||||
|
||||
def _scanFloat(self) -> Token|None:
|
||||
start = self._current
|
||||
char = self._currentChar()
|
||||
if char in SIGN_SET:
|
||||
self._nextLetter() # skip sign
|
||||
char = self._currentChar()
|
||||
|
||||
if char not in DIGIT_SET: return None
|
||||
|
||||
while self._currentChar() in DIGIT_SET:
|
||||
self._nextLetter()
|
||||
|
||||
if self._currentChar() != ".":
|
||||
return Token("float", self._text[start:self._current], self._line)
|
||||
|
||||
self._nextLetter() # skip '.'
|
||||
|
||||
while self._currentChar() in DIGIT_SET:
|
||||
self._nextLetter()
|
||||
|
||||
if self._currentChar() != "e":
|
||||
return Token("float", self._text[start:self._current], self._line)
|
||||
|
||||
self._nextLetter() # skip 'e'
|
||||
if char in SIGN_SET:
|
||||
self._nextLetter() # skip sign
|
||||
char = self._currentChar()
|
||||
|
||||
while self._currentChar() in DIGIT_SET:
|
||||
self._nextLetter()
|
||||
|
||||
return Token("float", self._text[start:self._current], self._line)
|
||||
|
||||
def _scanIdentifier(self) -> Token|None:
|
||||
if self._currentChar() not in IDENTIFIER_SET: return None
|
||||
start = self._current
|
||||
|
||||
while self._currentChar() in IDENTIFIER_SET:
|
||||
self._nextLetter()
|
||||
|
||||
return Token("identifier", self._text[start:self._current], self._line)
|
||||
|
||||
def _scanLetterSequence(self, name:str, text:str) -> Token|None:
|
||||
start = self._current
|
||||
for char in text:
|
||||
if self._currentChar() != char:
|
||||
self._current = start
|
||||
return None
|
||||
|
||||
self._nextLetter()
|
||||
|
||||
return Token(name, self._text[start:self._current], self._line)
|
||||
|
||||
def _scanLetterSet(self, name:str, letters:str) -> Token|None:
|
||||
if self._currentChar() not in letters: return None
|
||||
|
||||
start = self._current
|
||||
while self._currentChar() in letters:
|
||||
self._nextLetter()
|
||||
|
||||
return Token(name, self._text[start:self._current], self._line)
|
||||
|
||||
def _scanLineComment(self) -> Token|None:
|
||||
if self._currentChar() != self._comment: return None
|
||||
self._nextLetter() # skip comment char
|
||||
while self._currentChar() in " \t":
|
||||
self._nextLetter()
|
||||
|
||||
start = self._current
|
||||
while self._currentChar() != "\n":
|
||||
self._nextLetter()
|
||||
|
||||
return Token("comment", self._text[start:self._current], self._line)
|
||||
|
||||
def _scanOther(self) -> Token:
|
||||
start = self._current
|
||||
self._nextLetter()
|
||||
return Token("other", self._text[start:self._current], self._line)
|
||||
|
||||
def _scanQuotedText(self) -> Token|None:
|
||||
startChar = self._currentChar()
|
||||
if startChar not in self._quotes: return None
|
||||
|
||||
self._nextLetter() # skip starting quote
|
||||
start = self._current
|
||||
|
||||
chars:list[str] = []
|
||||
while True:
|
||||
char = self._currentChar()
|
||||
if char == self._escape:
|
||||
self._nextLetter() # skip escape char
|
||||
chars.append(self._currentChar())
|
||||
self._nextLetter() # skip following char
|
||||
char = self._currentChar()
|
||||
|
||||
if char == "":
|
||||
raise TokenizationError(f"Expected {startChar}, but found end of text", self._line)
|
||||
|
||||
if char == startChar:
|
||||
token = Token("quoted-text", "".join(chars), self._line)
|
||||
self._nextLetter() # skip final quote
|
||||
return token
|
||||
|
||||
chars.append(char)
|
||||
|
||||
self._nextLetter()
|
||||
@@ -0,0 +1,116 @@
|
||||
from pytest import fixture
|
||||
from mcpacker.format.tokenizer import Tokenizer
|
||||
|
||||
import textwrap
|
||||
|
||||
# Fixtures #########################################################################################
|
||||
|
||||
@fixture(name="basicJsonTokens")
|
||||
def createBasicJson():
|
||||
yield Tokenizer(textwrap.dedent("""
|
||||
{
|
||||
"alpha": {
|
||||
"foxtrot": {
|
||||
"bravo": "cha\\"rlie",
|
||||
"delta": 12.123,
|
||||
"echo": [
|
||||
1, 2, 3
|
||||
]
|
||||
}
|
||||
},
|
||||
"golf hotel": {
|
||||
"juliette": false
|
||||
}
|
||||
}
|
||||
""").strip()).all()
|
||||
|
||||
@fixture(name="basicTomlTokens")
|
||||
def createBasicToml():
|
||||
yield Tokenizer(textwrap.dedent("""
|
||||
[alpha.foxtrot]
|
||||
bravo = "cha\\"rlie"
|
||||
# comment
|
||||
delta = 12.123
|
||||
echo = [
|
||||
1, 2, 3
|
||||
]
|
||||
|
||||
["golf hotel"]
|
||||
india = { juliette = false }
|
||||
""").strip()).all()
|
||||
|
||||
# Tests ############################################################################################
|
||||
|
||||
def test_basicJson(basicJsonTokens):
|
||||
assert list(str(t) for t in basicJsonTokens) == [
|
||||
'Token{name: open-brace, text: "{", line: 0}}',
|
||||
'Token{name: quoted-text, text: "alpha", line: 1}}',
|
||||
'Token{name: colon, text: ":", line: 1}}',
|
||||
'Token{name: open-brace, text: "{", line: 1}}',
|
||||
'Token{name: quoted-text, text: "foxtrot", line: 2}}',
|
||||
'Token{name: colon, text: ":", line: 2}}',
|
||||
'Token{name: open-brace, text: "{", line: 2}}',
|
||||
'Token{name: quoted-text, text: "bravo", line: 3}}',
|
||||
'Token{name: colon, text: ":", line: 3}}',
|
||||
'Token{name: quoted-text, text: "cha"rlie", line: 3}}',
|
||||
'Token{name: comma, text: ",", line: 3}}',
|
||||
'Token{name: quoted-text, text: "delta", line: 4}}',
|
||||
'Token{name: colon, text: ":", line: 4}}',
|
||||
'Token{name: float, text: "12.123", line: 4}}',
|
||||
'Token{name: comma, text: ",", line: 4}}',
|
||||
'Token{name: quoted-text, text: "echo", line: 5}}',
|
||||
'Token{name: colon, text: ":", line: 5}}',
|
||||
'Token{name: open-bracket, text: "[", line: 5}}',
|
||||
'Token{name: float, text: "1", line: 6}}',
|
||||
'Token{name: comma, text: ",", line: 6}}',
|
||||
'Token{name: float, text: "2", line: 6}}',
|
||||
'Token{name: comma, text: ",", line: 6}}',
|
||||
'Token{name: float, text: "3", line: 6}}',
|
||||
'Token{name: close-bracket, text: "]", line: 7}}',
|
||||
'Token{name: close-brace, text: "}", line: 8}}',
|
||||
'Token{name: close-brace, text: "}", line: 9}}',
|
||||
'Token{name: comma, text: ",", line: 9}}',
|
||||
'Token{name: quoted-text, text: "golf hotel", line: 10}}',
|
||||
'Token{name: colon, text: ":", line: 10}}',
|
||||
'Token{name: open-brace, text: "{", line: 10}}',
|
||||
'Token{name: quoted-text, text: "juliette", line: 11}}',
|
||||
'Token{name: colon, text: ":", line: 11}}',
|
||||
'Token{name: bool, text: "false", line: 11}}',
|
||||
'Token{name: close-brace, text: "}", line: 12}}',
|
||||
'Token{name: close-brace, text: "}", line: 13}}',
|
||||
]
|
||||
|
||||
def test_basicToml(basicTomlTokens):
|
||||
assert list(str(t) for t in basicTomlTokens) == [
|
||||
'Token{name: open-bracket, text: "[", line: 0}}',
|
||||
'Token{name: identifier, text: "alpha", line: 0}}',
|
||||
'Token{name: period, text: ".", line: 0}}',
|
||||
'Token{name: identifier, text: "foxtrot", line: 0}}',
|
||||
'Token{name: close-bracket, text: "]", line: 0}}',
|
||||
'Token{name: identifier, text: "bravo", line: 1}}',
|
||||
'Token{name: equal, text: "=", line: 1}}',
|
||||
'Token{name: quoted-text, text: "cha"rlie", line: 1}}',
|
||||
'Token{name: comment, text: "comment", line: 2}}',
|
||||
'Token{name: identifier, text: "delta", line: 3}}',
|
||||
'Token{name: equal, text: "=", line: 3}}',
|
||||
'Token{name: float, text: "12.123", line: 3}}',
|
||||
'Token{name: identifier, text: "echo", line: 4}}',
|
||||
'Token{name: equal, text: "=", line: 4}}',
|
||||
'Token{name: open-bracket, text: "[", line: 4}}',
|
||||
'Token{name: float, text: "1", line: 5}}',
|
||||
'Token{name: comma, text: ",", line: 5}}',
|
||||
'Token{name: float, text: "2", line: 5}}',
|
||||
'Token{name: comma, text: ",", line: 5}}',
|
||||
'Token{name: float, text: "3", line: 5}}',
|
||||
'Token{name: close-bracket, text: "]", line: 6}}',
|
||||
'Token{name: open-bracket, text: "[", line: 8}}',
|
||||
'Token{name: quoted-text, text: "golf hotel", line: 8}}',
|
||||
'Token{name: close-bracket, text: "]", line: 8}}',
|
||||
'Token{name: identifier, text: "india", line: 9}}',
|
||||
'Token{name: equal, text: "=", line: 9}}',
|
||||
'Token{name: open-brace, text: "{", line: 9}}',
|
||||
'Token{name: identifier, text: "juliette", line: 9}}',
|
||||
'Token{name: equal, text: "=", line: 9}}',
|
||||
'Token{name: bool, text: "false", line: 9}}',
|
||||
'Token{name: close-brace, text: "}", line: 9}}',
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
from mcpacker.format.toml.model import TomlDocument
|
||||
from mcpacker.format.toml.parser import TomlParser
|
||||
from mcpacker.format.toml.composer import TomlComposer
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
def dumps(document:TomlDocument) -> str:
|
||||
return str(TomlComposer(document).compose())
|
||||
|
||||
def loads(text:str) -> TomlDocument:
|
||||
return TomlParser(text).parse()
|
||||
@@ -0,0 +1,7 @@
|
||||
import mcpacker.format.toml
|
||||
|
||||
|
||||
# Tests ############################################################################################
|
||||
|
||||
def test_syntax():
|
||||
pass
|
||||
@@ -0,0 +1,112 @@
|
||||
from mcpacker.format.textcomposer import TextComposer
|
||||
from mcpacker.format.toml.model import TomlArray
|
||||
from mcpacker.format.toml.model import TomlArrayEntry
|
||||
from mcpacker.format.toml.model import TomlComment
|
||||
from mcpacker.format.toml.model import TomlDocument
|
||||
from mcpacker.format.toml.model import TomlDocumentEntry
|
||||
from mcpacker.format.toml.model import TomlMap
|
||||
from mcpacker.format.toml.model import TomlMapEntry
|
||||
from mcpacker.format.toml.model import TomlPair
|
||||
from mcpacker.format.toml.model import TomlScalar
|
||||
from mcpacker.format.toml.model import TomlScalarValue
|
||||
from mcpacker.format.toml.model import TomlTable
|
||||
from mcpacker.format.toml.model import TomlTableEntry
|
||||
from mcpacker.format.toml.model import TomlTablePath
|
||||
from mcpacker.format.toml.model import TomlValue
|
||||
from typing import get_args
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class TomlComposer(TextComposer):
|
||||
|
||||
def __init__(self, document:TomlDocument):
|
||||
super().__init__()
|
||||
self._document = document
|
||||
|
||||
def doCompose(self):
|
||||
self.reset()
|
||||
|
||||
for entry in self._document.entries:
|
||||
if isinstance(entry, TomlComment): self._composeComment(entry)
|
||||
if isinstance(entry, TomlPair): self._composePair(entry)
|
||||
if isinstance(entry, TomlTable): self._composeTable(entry)
|
||||
|
||||
# Private Methods ##########################################################
|
||||
|
||||
def _composeComment(self, comment:TomlComment):
|
||||
self.text("# ").line(comment.text)
|
||||
|
||||
def _composeMap(self, tomlMap:TomlMap):
|
||||
self.text("{")
|
||||
self.indent()
|
||||
for entry in tomlMap.entries:
|
||||
if isinstance(entry, TomlComment): self._composeComment(entry)
|
||||
if isinstance(entry, TomlPair): self._composePair(entry)
|
||||
self.outdent()
|
||||
self.text("}")
|
||||
|
||||
def _composeArray(self, array:TomlArray):
|
||||
self.line("[")
|
||||
self.indent()
|
||||
needsDelimiter = False
|
||||
for element in array.entries:
|
||||
if needsDelimiter: self.line(",")
|
||||
needsDelimiter = True
|
||||
|
||||
if isinstance(element, TomlComment):
|
||||
self._composeComment(element)
|
||||
else:
|
||||
self._composeValue(element)
|
||||
|
||||
self.line()
|
||||
self.outdent()
|
||||
self.text("]")
|
||||
|
||||
def _composePair(self, pair:TomlPair):
|
||||
self.text(pair.key).text(" = ")
|
||||
if isinstance(pair.value, TomlScalar): self._composeScalar(pair.value)
|
||||
if isinstance(pair.value, TomlArray): self._composeArray(pair.value)
|
||||
if isinstance(pair.value, TomlMap): self._composeMap(pair.value)
|
||||
|
||||
def _composeScalar(self, scalar:TomlScalar):
|
||||
if isinstance(scalar.value, bool):
|
||||
self.text("true" if scalar.value else "false")
|
||||
elif isinstance(scalar.value, int):
|
||||
self.text(f"{scalar.value:d}")
|
||||
elif isinstance(scalar.value, float):
|
||||
self.text(f"{scalar.value:0.3f}")
|
||||
elif isinstance(scalar.value, str):
|
||||
value = (scalar.value
|
||||
.replace("\\", "\\\\")
|
||||
.replace('"', '\\"')
|
||||
.replace("\t", "\\t")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
)
|
||||
self.text("\"").text(value).text("\"")
|
||||
|
||||
def _composeTable(self, table:TomlTable):
|
||||
self.resetIndent()
|
||||
self.text("[")
|
||||
needsDelimiter = False
|
||||
for pathPart in table.path.parts:
|
||||
if needsDelimiter: self.text(".")
|
||||
needsDelimiter = True
|
||||
self.text(pathPart)
|
||||
self.line("]")
|
||||
|
||||
for entry in table.entries:
|
||||
if isinstance(entry, TomlComment):
|
||||
self._composeComment(entry)
|
||||
elif isinstance(entry, TomlPair):
|
||||
self._composePair(entry)
|
||||
self.line()
|
||||
|
||||
if len(table.entries) > 0:
|
||||
self.line()
|
||||
|
||||
def _composeValue(self, value:TomlValue):
|
||||
if isinstance(value, TomlScalar): self._composeScalar(value)
|
||||
if isinstance(value, TomlArray): self._composeArray(value)
|
||||
if isinstance(value, TomlMap): self._composeMap(value)
|
||||
@@ -0,0 +1,60 @@
|
||||
from mcpacker.format.toml.composer import TomlComposer
|
||||
from mcpacker.format.toml.model import TomlArray
|
||||
from mcpacker.format.toml.model import TomlArrayEntry
|
||||
from mcpacker.format.toml.model import TomlComment
|
||||
from mcpacker.format.toml.model import TomlDocument
|
||||
from mcpacker.format.toml.model import TomlDocumentEntry
|
||||
from mcpacker.format.toml.model import TomlMap
|
||||
from mcpacker.format.toml.model import TomlMapEntry
|
||||
from mcpacker.format.toml.model import TomlPair
|
||||
from mcpacker.format.toml.model import TomlScalar
|
||||
from mcpacker.format.toml.model import TomlScalarValue
|
||||
from mcpacker.format.toml.model import TomlTable
|
||||
from mcpacker.format.toml.model import TomlTableEntry
|
||||
from mcpacker.format.toml.model import TomlTablePath
|
||||
from mcpacker.format.toml.model import TomlValue
|
||||
from pytest import fixture
|
||||
|
||||
import textwrap
|
||||
|
||||
|
||||
# Fixtures #########################################################################################
|
||||
|
||||
@fixture(name="composer")
|
||||
def createComposer():
|
||||
yield TomlComposer(TomlDocument([
|
||||
TomlTable(TomlTablePath(['alpha', 'foxtrot']), [
|
||||
TomlPair('bravo', TomlScalar('cha\"rlie')),
|
||||
TomlComment('comment'),
|
||||
TomlPair('delta', TomlScalar(12.123)),
|
||||
TomlPair('echo', TomlArray([
|
||||
TomlScalar(1.0),
|
||||
TomlScalar(2.0),
|
||||
TomlScalar(3.0)
|
||||
]))
|
||||
]),
|
||||
TomlTable(TomlTablePath(['golf hotel']), [
|
||||
TomlPair('india', TomlMap([
|
||||
TomlPair('juliette', TomlScalar(False))
|
||||
]))
|
||||
])
|
||||
])).compose()
|
||||
|
||||
|
||||
# Tests ############################################################################################
|
||||
|
||||
def test_document(composer):
|
||||
assert str(composer) == textwrap.dedent("""
|
||||
[alpha.foxtrot]
|
||||
bravo = "cha\\"rlie"
|
||||
# comment
|
||||
delta = 12.123
|
||||
echo = [
|
||||
1.000,
|
||||
2.000,
|
||||
3.000
|
||||
]
|
||||
|
||||
[golf hotel]
|
||||
india = {juliette = false}
|
||||
""").lstrip()
|
||||
@@ -0,0 +1,130 @@
|
||||
from __future__ import annotations
|
||||
from collections.abc import Iterable
|
||||
from typing import Union
|
||||
|
||||
|
||||
# Type Support #####################################################################################
|
||||
|
||||
type TomlScalarValue = bool | int | float | str
|
||||
|
||||
type TomlValue = TomlScalar | TomlArray | TomlMap
|
||||
|
||||
type TomlArrayEntry = TomlComment | TomlValue
|
||||
|
||||
type TomlMapEntry = TomlComment | TomlPair
|
||||
|
||||
type TomlTableEntry = TomlComment | TomlPair
|
||||
|
||||
type TomlDocumentEntry = TomlComment | TomlPair | TomlTable
|
||||
|
||||
|
||||
# Classes ##########################################################################################
|
||||
|
||||
class TomlArray:
|
||||
|
||||
def __init__(self, entries:Iterable[TomlArrayEntry]):
|
||||
self.entries = entries
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"TomlArray{entries:[" +
|
||||
",".join(repr(e) for e in self.entries) +
|
||||
"]}"
|
||||
)
|
||||
|
||||
class TomlComment:
|
||||
|
||||
def __init__(self, text:str):
|
||||
self.text = text
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"TomlComment{{text:{repr(self.text)}}}"
|
||||
|
||||
class TomlDocument:
|
||||
|
||||
def __init__(self, entries:Iterable[TomlDocumentEntry]|None=None):
|
||||
self.entries = list(entries or [])
|
||||
|
||||
def findTable(self, name:str) -> TomlTable|None:
|
||||
for entry in self.entries:
|
||||
if isinstance(entry, TomlTable):
|
||||
if entry.name == name:
|
||||
return entry
|
||||
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"TomlDocument{entries:[" +
|
||||
",".join(repr(e) for e in self.entries) +
|
||||
"]}"
|
||||
)
|
||||
|
||||
class TomlMap:
|
||||
|
||||
def __init__(self, entries:Iterable[TomlMapEntry]):
|
||||
self.entries = entries
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"TomlMap{entries:[" +
|
||||
",".join(repr(e) for e in self.entries) +
|
||||
"]}"
|
||||
)
|
||||
|
||||
class TomlPair:
|
||||
|
||||
def __init__(self, key:str, value:TomlValue):
|
||||
self.key = key
|
||||
self.value = value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"TomlPair{{key:{repr(self.key)},value:{repr(self.value)}}}"
|
||||
|
||||
class TomlScalar:
|
||||
|
||||
def __init__(self, value:TomlScalarValue):
|
||||
self.value = value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"TomlScalar{{value:{repr(self.value)}}}"
|
||||
|
||||
class TomlTable:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path:TomlTablePath,
|
||||
entries:Iterable[TomlTableEntry]|None=None,
|
||||
):
|
||||
self.path = path
|
||||
self.entries = list(entries or [])
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return str(self.path)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"TomlTable{" +
|
||||
f"path:{repr(self.path)}," +
|
||||
f"entries:[" +
|
||||
",".join(repr(e) for e in self.entries) +
|
||||
"]" +
|
||||
"}"
|
||||
|
||||
)
|
||||
|
||||
class TomlTablePath:
|
||||
|
||||
def __init__(self, parts:Iterable[str]):
|
||||
self.parts = parts
|
||||
|
||||
def __str__(self) -> str:
|
||||
return ".".join(self.parts)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"TomlTablePath{parts:[" +
|
||||
".".join(repr(p) for p in self.parts) +
|
||||
"]}"
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
import mcpacker.format.toml.model
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
def test_syntax():
|
||||
pass
|
||||
@@ -0,0 +1,206 @@
|
||||
from mcpacker.format.tokenizer import Tokenizer
|
||||
from mcpacker.format.toml.model import TomlArray
|
||||
from mcpacker.format.toml.model import TomlArrayEntry
|
||||
from mcpacker.format.toml.model import TomlComment
|
||||
from mcpacker.format.toml.model import TomlDocument
|
||||
from mcpacker.format.toml.model import TomlDocumentEntry
|
||||
from mcpacker.format.toml.model import TomlMap
|
||||
from mcpacker.format.toml.model import TomlMapEntry
|
||||
from mcpacker.format.toml.model import TomlPair
|
||||
from mcpacker.format.toml.model import TomlScalar
|
||||
from mcpacker.format.toml.model import TomlScalarValue
|
||||
from mcpacker.format.toml.model import TomlTable
|
||||
from mcpacker.format.toml.model import TomlTableEntry
|
||||
from mcpacker.format.toml.model import TomlTablePath
|
||||
from mcpacker.format.toml.model import TomlValue
|
||||
|
||||
|
||||
# Helper Classes ###################################################################################
|
||||
|
||||
class TomlParseError(Exception):
|
||||
|
||||
def __init__(self, message:str, line:int):
|
||||
self.message = message
|
||||
self.line = line
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"TomlParseError:{self.line}: {self.message}"
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class TomlParser:
|
||||
|
||||
def __init__(self, text:str):
|
||||
self._tokenizer = Tokenizer(text)
|
||||
|
||||
def parse(self) -> TomlDocument:
|
||||
entries = []
|
||||
while True:
|
||||
entry = self._parseDocumentEntry()
|
||||
if not entry: break
|
||||
entries.append(entry)
|
||||
|
||||
return TomlDocument(entries)
|
||||
|
||||
# Private Methods ##########################################################
|
||||
|
||||
def _parseArray(self) -> TomlArray|None:
|
||||
openToken = self._tokenizer.next()
|
||||
if openToken.name != "open-bracket":
|
||||
self._tokenizer.reject(openToken)
|
||||
return None
|
||||
|
||||
entries = []
|
||||
while True:
|
||||
entry = self._parseArrayEntry()
|
||||
if not entry: break
|
||||
entries.append(entry)
|
||||
|
||||
commaToken = self._tokenizer.next()
|
||||
if commaToken.name != "comma":
|
||||
self._tokenizer.reject(commaToken)
|
||||
break
|
||||
|
||||
closeToken = self._tokenizer.next()
|
||||
if closeToken.name != "close-bracket":
|
||||
raise TomlParseError(f"expected ] but found {closeToken.text}", closeToken.line)
|
||||
|
||||
return TomlArray(entries)
|
||||
|
||||
def _parseArrayEntry(self) -> TomlArrayEntry|None:
|
||||
return self._parseComment() or self._parseValue()
|
||||
|
||||
def _parseComment(self) -> TomlComment|None:
|
||||
token = self._tokenizer.next()
|
||||
if token.name != "comment":
|
||||
self._tokenizer.reject(token)
|
||||
return None
|
||||
|
||||
return TomlComment(token.text)
|
||||
|
||||
def _parseDocumentEntry(self) -> TomlDocumentEntry|None:
|
||||
return self._parseComment() or self._parseTable() or self._parsePair()
|
||||
|
||||
def _parseMapEntry(self) -> TomlMapEntry|None:
|
||||
return self._parseComment() or self._parsePair()
|
||||
|
||||
def _parseMap(self) -> TomlMap|None:
|
||||
openToken = self._tokenizer.next()
|
||||
if openToken.name != "open-brace":
|
||||
self._tokenizer.reject(openToken)
|
||||
return None
|
||||
|
||||
entries = []
|
||||
while True:
|
||||
entry = self._parseMapEntry()
|
||||
if not entry: break
|
||||
entries.append(entry)
|
||||
|
||||
commaToken = self._tokenizer.next()
|
||||
if commaToken.name != "comma":
|
||||
self._tokenizer.reject(commaToken)
|
||||
break
|
||||
|
||||
closeToken = self._tokenizer.next()
|
||||
if closeToken.name != "close-brace":
|
||||
raise TomlParseError(f"expected }} but found {closeToken.text}", closeToken.line)
|
||||
|
||||
return TomlMap(entries)
|
||||
|
||||
def _parsePair(self) -> TomlPair|None:
|
||||
keyToken = self._tokenizer.next()
|
||||
if keyToken.name not in ["identifier", "quoted-text"]:
|
||||
self._tokenizer.reject(keyToken)
|
||||
return None
|
||||
|
||||
equalToken = self._tokenizer.next()
|
||||
if equalToken.name != "equal":
|
||||
self._tokenizer.reject(equalToken)
|
||||
self._tokenizer.reject(keyToken)
|
||||
return None
|
||||
|
||||
value = self._parseValue()
|
||||
if not value:
|
||||
self._tokenizer.reject(equalToken)
|
||||
self._tokenizer.reject(keyToken)
|
||||
return None
|
||||
|
||||
return TomlPair(keyToken.text, value)
|
||||
|
||||
def _parseScalar(self) -> TomlScalar|None:
|
||||
token = self._tokenizer.next()
|
||||
result = None
|
||||
|
||||
if token.name == "bool":
|
||||
result = TomlScalar(True if token.text.lower() == "true" else False)
|
||||
elif token.name == "int":
|
||||
result = TomlScalar(int(token.text))
|
||||
elif token.name == "float":
|
||||
result = TomlScalar(float(token.text))
|
||||
elif token.name == "quoted-text":
|
||||
result = TomlScalar(token.text)
|
||||
else:
|
||||
self._tokenizer.reject(token)
|
||||
|
||||
return result
|
||||
|
||||
def _parseTable(self) -> TomlTable|None:
|
||||
path = self._parseTablePath()
|
||||
|
||||
entries:list[TomlTableEntry] = []
|
||||
while True:
|
||||
entry = self._parseTableEntry()
|
||||
if not entry: break
|
||||
entries.append(entry)
|
||||
|
||||
if not (path or entries):
|
||||
return None
|
||||
elif not path:
|
||||
path = TomlTablePath([""])
|
||||
|
||||
return TomlTable(path, entries)
|
||||
|
||||
def _parseTableEntry(self) -> TomlTableEntry|None:
|
||||
return self._parseComment() or self._parsePair()
|
||||
|
||||
def _parseTablePath(self) -> TomlTablePath|None:
|
||||
openToken = self._tokenizer.next()
|
||||
if openToken.name != "open-bracket":
|
||||
self._tokenizer.reject(openToken)
|
||||
return None
|
||||
|
||||
quotedNameToken = self._tokenizer.next()
|
||||
if quotedNameToken.name == "quoted-text":
|
||||
closeToken = self._tokenizer.next()
|
||||
if closeToken.name != "close-bracket":
|
||||
raise TomlParseError(f"expected ], but found {closeToken.text}", closeToken.line)
|
||||
|
||||
return TomlTablePath([quotedNameToken.text])
|
||||
else:
|
||||
self._tokenizer.reject(quotedNameToken)
|
||||
|
||||
nameParts:list[str] = []
|
||||
while True:
|
||||
namePartToken = self._tokenizer.next()
|
||||
if namePartToken.name != "identifier":
|
||||
raise TomlParseError(
|
||||
f"expected a name part, but found {namePartToken.text}",
|
||||
namePartToken.line
|
||||
)
|
||||
|
||||
nameParts.append(namePartToken.text)
|
||||
|
||||
periodToken = self._tokenizer.next()
|
||||
if periodToken.name != "period":
|
||||
self._tokenizer.reject(periodToken)
|
||||
break
|
||||
|
||||
closeToken = self._tokenizer.next()
|
||||
if closeToken.name != "close-bracket":
|
||||
raise TomlParseError(f"expected ], but found {closeToken.text}", closeToken.line)
|
||||
|
||||
return TomlTablePath(nameParts)
|
||||
|
||||
def _parseValue(self) -> TomlValue|None:
|
||||
return self._parseScalar() or self._parseArray() or self._parseMap()
|
||||
@@ -0,0 +1,45 @@
|
||||
from pytest import fixture
|
||||
from mcpacker.format.toml.parser import TomlParser
|
||||
|
||||
import textwrap
|
||||
|
||||
|
||||
# Fixtures #########################################################################################
|
||||
|
||||
@fixture(name="basicTomlDocument")
|
||||
def createBasicTomlDocument():
|
||||
return TomlParser(textwrap.dedent("""
|
||||
[alpha.foxtrot]
|
||||
bravo = "cha\\"rlie"
|
||||
# comment
|
||||
delta = 12.123
|
||||
echo = [
|
||||
1, 2, 3
|
||||
]
|
||||
|
||||
["golf hotel"]
|
||||
india = { juliette = false }
|
||||
""").strip()).parse()
|
||||
|
||||
# Tests ############################################################################################
|
||||
|
||||
def test_basicTomlDocument(basicTomlDocument):
|
||||
assert repr(basicTomlDocument) == (
|
||||
"TomlDocument{entries:[" +
|
||||
"TomlTable{path:TomlTablePath{parts:['alpha'.'foxtrot']},entries:[" +
|
||||
"TomlPair{key:'bravo',value:TomlScalar{value:'cha\"rlie'}}," +
|
||||
"TomlComment{text:'comment'}," +
|
||||
"TomlPair{key:'delta',value:TomlScalar{value:12.123}}," +
|
||||
"TomlPair{key:'echo',value:TomlArray{entries:[" +
|
||||
"TomlScalar{value:1.0}," +
|
||||
"TomlScalar{value:2.0}," +
|
||||
"TomlScalar{value:3.0}" +
|
||||
"]}}" +
|
||||
"]}," +
|
||||
"TomlTable{path:TomlTablePath{parts:['golf hotel']},entries:[" +
|
||||
"TomlPair{key:'india',value:TomlMap{entries:[" +
|
||||
"TomlPair{key:'juliette',value:TomlScalar{value:False}}" +
|
||||
"]}}" +
|
||||
"]}" +
|
||||
"]}"
|
||||
)
|
||||
@@ -3,6 +3,7 @@ from mcpacker.model.resourcepack.variant import Variant
|
||||
from mcpacker.model.resourcepack.model import Model
|
||||
from mcpacker.model.modpack import ModPack
|
||||
from mcpacker.model.core.resourceid import ResourceId
|
||||
from mcpacker.model.datapack.recipe.shapedrecipe import ShapedRecipe
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
@@ -23,4 +24,3 @@ class BlockFactory:
|
||||
resource.blockStates.append(BlockState(Variant(blockId)))
|
||||
|
||||
data = self.pack.dataPack.get(gameId.mod)
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class Altitude:
|
||||
@@ -25,6 +28,7 @@ class Altitude:
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.name} <{self.bottom} to {self.top}>"
|
||||
|
||||
|
||||
# Constants ########################################################################################
|
||||
|
||||
# Aboveground
|
||||
@@ -61,17 +65,18 @@ OVERGROUND = (DUNES, LOWLANDS, UPLANDS, HILLS, ALPINE, CRAGS, PEAKS, SUMMIT, SK
|
||||
UNDERGROUND = (SOIL, SUBSTRATE, OVERBURDEN, CRUST, MANTLE, PLUTONIC)
|
||||
UNDERWATER = (SURFACE, SHALLOWS, DEEPS, ABYSS)
|
||||
|
||||
|
||||
# Helper Functions #################################################################################
|
||||
|
||||
def span(*altitudes):
|
||||
def span(*altitudes:Altitude):
|
||||
if not altitudes: return ANYWHERE
|
||||
result = Altitude("", ANYWHERE.top, ANYWHERE.bottom)
|
||||
|
||||
bottom = OVERGROUND[-1].top
|
||||
bottomAltitude = None
|
||||
bottomAltitude = OVERGROUND[-1]
|
||||
|
||||
top = UNDERGROUND[-1].bottom
|
||||
topAltitude = None
|
||||
topAltitude = UNDERGROUND[-1]
|
||||
|
||||
for altitude in altitudes:
|
||||
if altitude.bottom < bottom:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Sequence
|
||||
from mcpacker.model.core.ecology.biome import Biome
|
||||
from mcpacker.model.core.ecology.biometrait import BiomeTrait
|
||||
|
||||
@@ -26,8 +26,8 @@ class BiomeFilter:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
required:Iterable[BiomeTrait|Iterable[BiomeTrait]]|None=None,
|
||||
prohibited:Iterable[BiomeTrait]|None=None,
|
||||
required:Sequence[BiomeTrait|Sequence[BiomeTrait]]|None=None,
|
||||
prohibited:Sequence[BiomeTrait]|None=None,
|
||||
):
|
||||
self.required = required or []
|
||||
self.prohibited = prohibited or []
|
||||
|
||||
@@ -39,14 +39,14 @@ ALL = [SOLO, PAIR, FAMILY, TROUP, HERD]
|
||||
|
||||
# Helper Functions #################################################################################
|
||||
|
||||
def merge(*groups):
|
||||
def merge(*groups:Group):
|
||||
if not groups: return SOLO
|
||||
|
||||
smallest = ALL[-1].largest
|
||||
smallestGroup = None
|
||||
smallestGroup = ALL[-1]
|
||||
|
||||
largest = ALL[0].smallest
|
||||
largestGroup = None
|
||||
largestGroup = ALL[0]
|
||||
|
||||
for group in groups:
|
||||
if group.smallest < smallest:
|
||||
|
||||
@@ -4,19 +4,20 @@ from mcpacker.model.core.fauna.mobspawn import MobSpawn
|
||||
from mcpacker.model.core.habitat import Habitat
|
||||
from pytest import fixture
|
||||
|
||||
import mcpacker.model.core.ecology.flora as F
|
||||
import mcpacker.model.core.ecology.heat as E
|
||||
import mcpacker.model.core.fauna.active as AC
|
||||
import mcpacker.model.core.ecology.flora as FL
|
||||
import mcpacker.model.core.ecology.heat as HE
|
||||
|
||||
|
||||
# Fixtures #########################################################################################
|
||||
|
||||
@fixture(name="cow")
|
||||
def createCow():
|
||||
yield Mob("cow", "minecraft:cow")
|
||||
yield Mob("minecraft:cow", AC.DIURNAL)
|
||||
|
||||
@fixture(name="fields")
|
||||
def createFieldsHabitat():
|
||||
yield Habitat(biomeFilter=BiomeFilter([F.FIELD, E.TEMPERATE]))
|
||||
yield Habitat(biomeFilter=BiomeFilter([FL.FIELD, HE.TEMPERATE]))
|
||||
|
||||
@fixture(name="spawn")
|
||||
def createMobSpawn(cow, fields):
|
||||
|
||||
@@ -39,7 +39,7 @@ class Habitat:
|
||||
self.location = location
|
||||
self.scarcity = scarcity
|
||||
|
||||
self._source = None
|
||||
self._source:Habitat|None = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "".join([str(p) for p in [
|
||||
@@ -63,17 +63,24 @@ class Habitat:
|
||||
"}"
|
||||
]])
|
||||
|
||||
def derive(self, **kwargs):
|
||||
kwargs = {
|
||||
"altitude": self.altitude,
|
||||
"biomeFilter": self.biomeFilter,
|
||||
"seasons": self.seasons,
|
||||
"group": self.group,
|
||||
"location": self.location,
|
||||
"scarcity": self.scarcity
|
||||
} | kwargs
|
||||
|
||||
result = Habitat(**kwargs)
|
||||
def derive(
|
||||
self,
|
||||
*,
|
||||
altitude:Altitude|None=None,
|
||||
biomeFilter:BiomeFilter|None=None,
|
||||
seasons:Iterable[Season]|Season|None=None,
|
||||
group:Group|None=None,
|
||||
location:Location|None=None,
|
||||
scarcity:Scarcity|None=None,
|
||||
):
|
||||
result = Habitat(
|
||||
altitude or self.altitude,
|
||||
biomeFilter or self.biomeFilter,
|
||||
seasons or self.seasons,
|
||||
group or self.group,
|
||||
location or self.location,
|
||||
scarcity or self.scarcity
|
||||
)
|
||||
result._source = self
|
||||
|
||||
return result
|
||||
|
||||
@@ -5,6 +5,11 @@ from mcpacker.model.datapack.moddata import ModData
|
||||
# Class ############################################################################################
|
||||
|
||||
class DataPack:
|
||||
"""
|
||||
A collection of data used to configure a Minecraft instance.
|
||||
|
||||
see: https://minecraft.wiki/w/Data_pack
|
||||
"""
|
||||
|
||||
def __init__(self, name:str, mods:Iterable[ModData]|None=None):
|
||||
self.name = name
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Mapping
|
||||
from mcpacker.model.core.resourceid import ResourceId
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class Recipe:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gameId:str|ResourceId,
|
||||
resultId:str|ResourceId,
|
||||
resultCount:int=1,
|
||||
resultComponents:Mapping[str,str]|None=None,
|
||||
):
|
||||
self.gameId = ResourceId.parse(gameId)
|
||||
self.resultComponents = resultComponents
|
||||
self.resultCount = resultCount
|
||||
self.resultId = ResourceId.parse(resultId)
|
||||
@@ -0,0 +1,56 @@
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Mapping
|
||||
from mcpacker.model.datapack.recipe import Recipe
|
||||
from mcpacker.model.core.resourceid import ResourceId
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class ShapedRecipe(Recipe):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gameId:str|ResourceId,
|
||||
key:Mapping[str,(str|ResourceId|Iterable[ResourceId|str])],
|
||||
pattern:Iterable[str],
|
||||
resultId:str|ResourceId,
|
||||
resultCount:int=1,
|
||||
resultComponents:Mapping[str,str]|None=None,
|
||||
):
|
||||
super().__init__(gameId, resultId, resultCount, resultComponents)
|
||||
self.key = self._parseKey(key)
|
||||
self.pattern = self._parsePattern(pattern)
|
||||
|
||||
# Private Methods ##########################################################
|
||||
|
||||
def _parseKey(
|
||||
self,
|
||||
key:Mapping[str,(str|ResourceId|Iterable[ResourceId|str])]
|
||||
) -> dict[str, list[ResourceId]]:
|
||||
result:dict[str, list[ResourceId]] = {}
|
||||
|
||||
for name, value in key.items():
|
||||
finalValue:list[ResourceId] = []
|
||||
|
||||
if isinstance(value, str) or isinstance(value, ResourceId):
|
||||
finalValue = [ ResourceId.parse(value) ]
|
||||
elif isinstance(value, Iterable):
|
||||
finalValue = [ ResourceId.parse(e) for e in value ]
|
||||
|
||||
if len(name) != 1:
|
||||
raise ValueError(f"key names must be a single letter (\"{name}\" is not valid)")
|
||||
|
||||
result[name] = finalValue
|
||||
|
||||
return result
|
||||
|
||||
def _parsePattern(self, pattern:Iterable[str]) -> list[str]:
|
||||
result:list[str] = []
|
||||
|
||||
for row in pattern:
|
||||
if len(row) not in [2, 3]:
|
||||
raise ValueError(f"pattern rows must be 2 or 3 letters: (\"{row}\" is not valid)")
|
||||
|
||||
result.append(row)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,27 @@
|
||||
from pytest import fixture
|
||||
from mcpacker.model.datapack.recipe.shapedrecipe import ShapedRecipe
|
||||
|
||||
|
||||
# Fixtures #########################################################################################
|
||||
|
||||
@fixture(name="recipe")
|
||||
def createRecipe():
|
||||
yield ShapedRecipe(
|
||||
"iron_pickaxe",
|
||||
{"s": "stick", "i": "iron_ingot"},
|
||||
["iii", " s ", " s "],
|
||||
"iron_pickaxe"
|
||||
)
|
||||
|
||||
|
||||
# Tests ############################################################################################
|
||||
|
||||
def test_recipe(recipe:ShapedRecipe):
|
||||
keyText = ", ".join(f"{k}:{str(v[0])}" for k,v in recipe.key.items())
|
||||
recipeText = "|".join(str(i) for i in recipe.pattern)
|
||||
|
||||
assert keyText == "s:minecraft:stick, i:minecraft:iron_ingot"
|
||||
assert recipeText == "iii| s | s "
|
||||
assert str(recipe.resultId) == "minecraft:iron_pickaxe"
|
||||
assert recipe.resultCount == 1
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Mapping
|
||||
from mcpacker.model.core.resourceid import ResourceId
|
||||
from mcpacker.model.datapack.recipe import Recipe
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class ShapelessRecipe(Recipe):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gameId:str|ResourceId,
|
||||
ingredients:Iterable[str|ResourceId],
|
||||
resultId:str|ResourceId,
|
||||
resultCount:int=1,
|
||||
resultComponents:Mapping[str,str]|None=None,
|
||||
):
|
||||
super().__init__(gameId, resultId, resultCount, resultComponents)
|
||||
self.ingredients = self._parseIngredients(ingredients)
|
||||
|
||||
def _parseIngredients(self, ingredients:Iterable[str|ResourceId]) -> list[ResourceId]:
|
||||
result = [ ResourceId.parse(e) for e in ingredients ]
|
||||
|
||||
if len(result) > 9:
|
||||
raise ValueError("cannot have more than 9 ingredients (\"{len(result}\" is not valid")
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,17 @@
|
||||
from pytest import fixture
|
||||
from mcpacker.model.datapack.recipe.shapelessrecipe import ShapelessRecipe
|
||||
|
||||
|
||||
# Fixtures #########################################################################################
|
||||
|
||||
@fixture(name="recipe")
|
||||
def createRecipe():
|
||||
yield ShapelessRecipe("flint_and_steel", ["iron_ingot", "flint"], "flint_and_steel")
|
||||
|
||||
|
||||
# Tests ############################################################################################
|
||||
|
||||
def test_recipe(recipe:ShapelessRecipe):
|
||||
assert ", ".join(str(i) for i in recipe.ingredients) == "minecraft:iron_ingot, minecraft:flint"
|
||||
assert str(recipe.resultId) == "minecraft:flint_and_steel"
|
||||
assert recipe.resultCount == 1
|
||||
@@ -4,6 +4,12 @@ from mcpacker.model.datapack import DataPack
|
||||
from mcpacker.model.mod import Mod
|
||||
from mcpacker.model.resourcepack import ResourcePack
|
||||
from typing import Callable
|
||||
from typing import Self
|
||||
|
||||
|
||||
# Type Helpers #####################################################################################
|
||||
|
||||
type AugmentFunc = Callable[[ModPack], None]
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
@@ -27,6 +33,6 @@ class ModPack:
|
||||
for mod in (mods or []):
|
||||
self._mods[mod.name] = mod
|
||||
|
||||
def augment(self, doAugment:"Callable[[ModPack], None]") -> "ModPack":
|
||||
def augment(self, doAugment:AugmentFunc) -> Self:
|
||||
doAugment(self)
|
||||
return self
|
||||
|
||||
+10
-11
@@ -1,15 +1,14 @@
|
||||
from mcpacker.format.report.biomereport import BiomeReport
|
||||
from mcpacker.format.report.mineralreport import MineralReport
|
||||
from mcpacker.format.report.mobspawnreport import MobSpawnReport
|
||||
from mcpacker.model.modpack import ModPack
|
||||
from mcpacker.write.incontrol.spawnerwriter import SpawnerWriter
|
||||
from mcpacker.write.markdown.biomereport import BiomeReport
|
||||
from mcpacker.write.markdown.mineralreport import MineralReport
|
||||
from mcpacker.write.markdown.mobspawnreport import MobSpawnReport
|
||||
from mcpacker.write.markdown.reportwriter import ReportWriter
|
||||
from mcpacker.write.compositewriter import CompositeWriter
|
||||
from mcpacker.write.incontrol.spawnerwriter import SpawnerWriter
|
||||
from mcpacker.write.report.writer import ReportWriter
|
||||
from mcpacker.write.staticwriter import StaticWriter
|
||||
from pathlib import Path
|
||||
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
@@ -46,13 +45,13 @@ class Runner:
|
||||
|
||||
def _command_writeReports(self):
|
||||
CompositeWriter(self.pack, self.outputDir, [
|
||||
BiomeReport,
|
||||
MineralReport,
|
||||
MobSpawnReport,
|
||||
ReportWriter(self.pack, BiomeReport, self.outputDir, "biomes.md"),
|
||||
ReportWriter(self.pack, MineralReport, self.outputDir, "minerals.md"),
|
||||
ReportWriter(self.pack, MobSpawnReport, self.outputDir, "mobspawns.md"),
|
||||
]).write()
|
||||
|
||||
def _command_writeModPack(self):
|
||||
CompositeWriter(self.pack, self.outputDir, [
|
||||
StaticWriter,
|
||||
SpawnerWriter,
|
||||
StaticWriter(self.pack, self.outputDir),
|
||||
SpawnerWriter(self.pack, self.outputDir),
|
||||
]).write()
|
||||
|
||||
@@ -2,15 +2,6 @@ from collections.abc import Iterable
|
||||
from mcpacker.model.modpack import ModPack
|
||||
from mcpacker.write.writer import Writer
|
||||
from pathlib import Path
|
||||
from typing import TypeVar
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
# Type Support #####################################################################################
|
||||
|
||||
WriterSubclass = TypeVar("WriterSubclass", bound=Writer)
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
@@ -21,16 +12,10 @@ class CompositeWriter(Writer):
|
||||
self,
|
||||
pack:ModPack,
|
||||
outputDir:Path,
|
||||
writers:Iterable[type[WriterSubclass]]|None=None
|
||||
writers:Iterable[Writer]|None=None
|
||||
):
|
||||
super().__init__(pack, outputDir)
|
||||
|
||||
self._writers:list[Writer] = []
|
||||
for writerClass in (writers or []):
|
||||
self.add(writerClass)
|
||||
|
||||
def add(self, writerClass:type[WriterSubclass]):
|
||||
self._writers.append(writerClass(self.pack, self.outputDir))
|
||||
self._writers:list[Writer] = list(w for w in writers or [])
|
||||
|
||||
def write(self):
|
||||
for writer in self._writers:
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
from mcpacker.write.writer import Writer
|
||||
from mcpacker.model.modpack import ModPack
|
||||
from pathlib import Path
|
||||
from typing import TextIO
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
# Constants ########################################################################################
|
||||
|
||||
INDENT = " "
|
||||
REPORT_DIR_PATH = "reports"
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class MarkdownWriter(Writer):
|
||||
|
||||
def __init__(self, name:str, pack:ModPack, outputDir:Path):
|
||||
super().__init__(pack, outputDir)
|
||||
|
||||
self._file:TextIO|None = None
|
||||
self._name = name
|
||||
self._indent = 0
|
||||
self._lineBuffer:list[str] = []
|
||||
|
||||
def write(self):
|
||||
path = self.outputDir / self.pack.name / REPORT_DIR_PATH / self._name
|
||||
|
||||
path.parent.mkdir(exist_ok=True, parents=True)
|
||||
with path.open("w") as file:
|
||||
try:
|
||||
self._file = file
|
||||
self.compose()
|
||||
finally:
|
||||
self._file = None
|
||||
|
||||
def compose(self) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
# Helper Methods ###########################################################
|
||||
|
||||
def indent(self) -> "MarkdownWriter":
|
||||
self._indent += 1
|
||||
self._lineBuffer.insert(0, INDENT)
|
||||
return self
|
||||
|
||||
def line(self, text:str="") -> "MarkdownWriter":
|
||||
assert self._file != None
|
||||
self.text(text)
|
||||
|
||||
self._file.write("".join(self._lineBuffer) + "\n")
|
||||
self._lineBuffer = []
|
||||
while len(self._lineBuffer) < self._indent:
|
||||
self._lineBuffer.append(INDENT)
|
||||
|
||||
return self
|
||||
|
||||
def outdent(self) -> "MarkdownWriter":
|
||||
self._indent = max(0, self._indent - 1)
|
||||
|
||||
if self._lineBuffer:
|
||||
if self._lineBuffer[0] == INDENT:
|
||||
self._lineBuffer.pop()
|
||||
|
||||
return self
|
||||
|
||||
def text(self, text:str) -> "MarkdownWriter":
|
||||
self._lineBuffer.append(str(text))
|
||||
return self
|
||||
@@ -1,43 +0,0 @@
|
||||
from mcpacker.model.modpack import ModPack
|
||||
from mcpacker.write.markdown.markdownwriter import MarkdownWriter
|
||||
from pathlib import Path
|
||||
from pytest import fixture
|
||||
|
||||
import textwrap
|
||||
|
||||
|
||||
# Helpers ##########################################################################################
|
||||
|
||||
class SampleWriter(MarkdownWriter):
|
||||
|
||||
def compose(self):
|
||||
(self.text("alpha")
|
||||
.line("bravo")
|
||||
.indent()
|
||||
.line("charlie")
|
||||
.indent()
|
||||
.line("delta")
|
||||
.outdent()
|
||||
.line("echo")
|
||||
)
|
||||
|
||||
|
||||
# Fixtures #########################################################################################
|
||||
|
||||
@fixture(name="writer")
|
||||
def createWriter(tmp_path:Path):
|
||||
writer = SampleWriter("testReport.md", ModPack("testModPack"), tmp_path)
|
||||
writer.write()
|
||||
yield writer
|
||||
|
||||
|
||||
# Tests ############################################################################################
|
||||
|
||||
def test_write(tmp_path, writer):
|
||||
path = tmp_path / "testModPack" / "reports" / "testReport.md"
|
||||
assert path.read_text() == textwrap.dedent("""
|
||||
alphabravo
|
||||
charlie
|
||||
delta
|
||||
echo
|
||||
""").lstrip()
|
||||
@@ -1,72 +0,0 @@
|
||||
from collections.abc import Iterator
|
||||
from mcpacker.model.modpack import ModPack
|
||||
|
||||
|
||||
# Constants ########################################################################################
|
||||
|
||||
INDENT = " "
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class Report:
|
||||
|
||||
def __init__(self, name:str, pack:ModPack):
|
||||
self.name = name
|
||||
self.pack = pack
|
||||
self.reset()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "\n".join(self._lines).strip()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
"Report{" +
|
||||
f"lines: {len(self._lines)} lines, " +
|
||||
f"lineBuffer: {len(self._lineBuffer)} chunks, " +
|
||||
f"indent: {self._indent}" +
|
||||
"}"
|
||||
)
|
||||
|
||||
def build(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def reset(self) -> "Report":
|
||||
self._lineBuffer:list[str] = []
|
||||
self._lines:list[str] = []
|
||||
self._indent = 0
|
||||
return self
|
||||
|
||||
def asLines(self) -> Iterator[str]:
|
||||
for line in self._lines:
|
||||
yield line
|
||||
|
||||
# Helper Methods ###########################################################
|
||||
|
||||
def indent(self) -> "Report":
|
||||
self._indent += 1
|
||||
self._lineBuffer.insert(0, INDENT)
|
||||
return self
|
||||
|
||||
def line(self, text:str="") -> "Report":
|
||||
self.text(text)
|
||||
|
||||
self._lines.append("".join(self._lineBuffer))
|
||||
self._lineBuffer = []
|
||||
while len(self._lineBuffer) < self._indent:
|
||||
self._lineBuffer.append(INDENT)
|
||||
|
||||
return self
|
||||
|
||||
def outdent(self) -> "Report":
|
||||
self._indent = max(0, self._indent - 1)
|
||||
|
||||
if self._lineBuffer:
|
||||
if self._lineBuffer[0] == INDENT:
|
||||
self._lineBuffer.pop()
|
||||
|
||||
return self
|
||||
|
||||
def text(self, text:str) -> "Report":
|
||||
self._lineBuffer.append(str(text))
|
||||
return self
|
||||
@@ -1,27 +0,0 @@
|
||||
from mcpacker.write.markdown.report import Report
|
||||
from pytest import fixture
|
||||
|
||||
|
||||
# Fixtures #########################################################################################
|
||||
|
||||
@fixture(name="report")
|
||||
def createReport():
|
||||
return (
|
||||
Report("test", None)
|
||||
.text("alpha")
|
||||
.line("bravo")
|
||||
.indent()
|
||||
.line("charlie")
|
||||
.indent()
|
||||
.line("delta")
|
||||
.outdent()
|
||||
.line("echo")
|
||||
)
|
||||
|
||||
# Tests ############################################################################################
|
||||
|
||||
def test_repr(report):
|
||||
assert repr(report) == "Report{lines: 4 lines, lineBuffer: 1 chunks, indent: 1}"
|
||||
|
||||
def test_str(report):
|
||||
assert str(report) == "alphabravo\n charlie\n delta\n echo"
|
||||
@@ -1,25 +0,0 @@
|
||||
from mcpacker.write.markdown.report import Report
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class ReportWriter:
|
||||
|
||||
def __init__(self, reports:list[Report]):
|
||||
self._reports = reports
|
||||
|
||||
def write(self, reportDirPath:str):
|
||||
if os.path.exists(reportDirPath):
|
||||
shutil.rmtree(reportDirPath)
|
||||
|
||||
os.makedirs(reportDirPath, exist_ok=True)
|
||||
|
||||
for report in self._reports:
|
||||
report.build()
|
||||
|
||||
path = os.path.join(reportDirPath, report.name)
|
||||
with open(path, "w") as file:
|
||||
file.write(str(report))
|
||||
@@ -0,0 +1,31 @@
|
||||
from mcpacker.model.modpack import ModPack
|
||||
from mcpacker.format.report.composer import ReportComposer
|
||||
from mcpacker.write.writer import Writer
|
||||
from pathlib import Path
|
||||
from typing import Self
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class ReportWriter(Writer):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pack:ModPack,
|
||||
composerClass:type[ReportComposer],
|
||||
outputDir:Path,
|
||||
name:str|Path
|
||||
):
|
||||
super().__init__(pack, outputDir)
|
||||
self.composerClass = composerClass
|
||||
self.name = Path(name)
|
||||
|
||||
def write(self) -> Self:
|
||||
path = self.outputDir/self.pack.name/"reports"/self.name
|
||||
self.resetOutputFile(path)
|
||||
|
||||
composer = self.composerClass(self.pack)
|
||||
with path.open("w") as file:
|
||||
file.write(str(composer.compose()))
|
||||
|
||||
return self
|
||||
@@ -0,0 +1,26 @@
|
||||
from mcpacker.format.textcomposer import TextComposer
|
||||
from mcpacker.model.modpack import ModPack
|
||||
from mcpacker.write.writer import Writer
|
||||
from pathlib import Path
|
||||
from typing import Self
|
||||
|
||||
import os
|
||||
|
||||
|
||||
# Class ############################################################################################
|
||||
|
||||
class TextWriter(Writer):
|
||||
|
||||
def __init__(self, pack:ModPack, outputDir:Path, filePath:Path, composer:TextComposer):
|
||||
super().__init__(pack, outputDir)
|
||||
self.composer = composer
|
||||
self.filePath = filePath
|
||||
|
||||
def write(self) -> Self:
|
||||
path = self.outputDir/self.pack.name/self.filePath
|
||||
self.resetOutputFile(path)
|
||||
|
||||
with path.open("w") as file:
|
||||
file.write(str(self.composer.compose()))
|
||||
|
||||
return self
|
||||
@@ -0,0 +1,37 @@
|
||||
from mcpacker.format.textcomposer import TextComposer
|
||||
from mcpacker.model.modpack import ModPack
|
||||
from mcpacker.write.textwriter import TextWriter
|
||||
from pathlib import Path
|
||||
from pytest import fixture
|
||||
|
||||
|
||||
# Helper Classes ###################################################################################
|
||||
|
||||
class SampleComposer(TextComposer):
|
||||
|
||||
def doCompose(self):
|
||||
self.line("alpha bravo")
|
||||
self.line("charlie")
|
||||
|
||||
|
||||
# Fixtures #########################################################################################
|
||||
|
||||
@fixture(name="composer")
|
||||
def createComposer():
|
||||
yield SampleComposer()
|
||||
|
||||
@fixture(name="pack")
|
||||
def createModPack():
|
||||
yield ModPack("testModPack")
|
||||
|
||||
@fixture(name="writer")
|
||||
def createWriter(pack:ModPack, tmp_path:Path, composer:TextComposer):
|
||||
yield TextWriter(pack, tmp_path, Path("reports")/"test.md", composer).write()
|
||||
|
||||
|
||||
# Tests ############################################################################################
|
||||
|
||||
def test_writer(tmp_path:Path, writer:TextWriter):
|
||||
path = tmp_path/"testModPack"/"reports"/"test.md"
|
||||
assert path.read_text() == "alpha bravo\ncharlie"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from mcpacker.model.modpack import ModPack
|
||||
from pathlib import Path
|
||||
from typing import Self
|
||||
|
||||
import os
|
||||
import shutil
|
||||
@@ -18,5 +19,5 @@ class Writer:
|
||||
os.remove(file)
|
||||
file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def write(self):
|
||||
def write(self) -> Self:
|
||||
raise NotImplementedError()
|
||||
|
||||
Reference in New Issue
Block a user