Refactor writing/formatting systems

This commit is contained in:
Andrew Miner
2025-10-13 20:59:15 -06:00
parent 516748bee5
commit 25b1b7f9f9
48 changed files with 1463 additions and 417 deletions
View File
@@ -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()
@@ -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()
+11
View File
@@ -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,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()
@@ -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()
@@ -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()
@@ -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()
+63
View File
@@ -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
+31
View File
@@ -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()
+254
View File
@@ -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()
+116
View File
@@ -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}}',
]
+12
View File
@@ -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()
+7
View File
@@ -0,0 +1,7 @@
import mcpacker.format.toml
# Tests ############################################################################################
def test_syntax():
pass
+112
View File
@@ -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)
+60
View File
@@ -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()
+130
View File
@@ -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) +
"]}"
)
+7
View File
@@ -0,0 +1,7 @@
import mcpacker.format.toml.model
# Class ############################################################################################
def test_syntax():
pass
+206
View File
@@ -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()
+45
View File
@@ -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}}" +
"]}}" +
"]}" +
"]}"
)
+1 -1
View File
@@ -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)
+8 -3
View File
@@ -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:
+3 -3
View File
@@ -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 []
+3 -3
View File
@@ -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:
+5 -4
View File
@@ -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):
+19 -12
View File
@@ -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
View File
@@ -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
+7 -1
View File
@@ -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
View File
@@ -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 -17
View File
@@ -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:
-71
View File
@@ -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()
-72
View File
@@ -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
-27
View File
@@ -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"
-25
View File
@@ -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))
View File
+31
View File
@@ -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
+26
View File
@@ -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
+37
View File
@@ -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"
+2 -1
View File
@@ -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()