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.model.modpack import ModPack
|
||||||
|
from mcpacker.format.report.composer import ReportComposer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
# Class ############################################################################################
|
# Class ############################################################################################
|
||||||
|
|
||||||
class BiomeReport(MarkdownWriter):
|
class BiomeReport(ReportComposer):
|
||||||
|
|
||||||
def __init__(self, pack:ModPack, outputDir:Path):
|
def doCompose(self):
|
||||||
super().__init__("biomes.md", pack, outputDir)
|
|
||||||
|
|
||||||
def compose(self):
|
|
||||||
for biome in self.pack.world.biomes:
|
for biome in self.pack.world.biomes:
|
||||||
self.line(f"# Biome: {biome.gameId} ({biome.city})")
|
self.line(f"# Biome: {biome.gameId} ({biome.city})")
|
||||||
self.line()
|
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.core.ecology.biome import Biome
|
||||||
|
from mcpacker.model.modpack import AugmentFunc
|
||||||
from mcpacker.model.modpack import ModPack
|
from mcpacker.model.modpack import ModPack
|
||||||
from pytest import fixture
|
from pytest import fixture
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import mcpacker.model.core.ecology.flora as F
|
import mcpacker.model.core.ecology.flora as F
|
||||||
import mcpacker.model.core.ecology.geology as G
|
import mcpacker.model.core.ecology.geology as G
|
||||||
@@ -32,22 +34,20 @@ def defineAddBiomes():
|
|||||||
return addBiomes
|
return addBiomes
|
||||||
|
|
||||||
@fixture(name="pack")
|
@fixture(name="pack")
|
||||||
def createPack(addBiomes):
|
def createPack(addBiomes:AugmentFunc):
|
||||||
pack = ModPack("testModPack")
|
pack = ModPack("testModPack")
|
||||||
pack.augment(addBiomes)
|
pack.augment(addBiomes)
|
||||||
yield pack
|
yield pack
|
||||||
|
|
||||||
@fixture(name="report")
|
@fixture(name="report")
|
||||||
def createReport(pack, tmp_path):
|
def createReport(pack:ModPack):
|
||||||
report = BiomeReport(pack, tmp_path)
|
yield BiomeReport(pack).compose()
|
||||||
report.write()
|
|
||||||
yield report
|
|
||||||
|
|
||||||
# Tests ############################################################################################
|
# Tests ############################################################################################
|
||||||
|
|
||||||
def test_report(tmp_path, report):
|
def test_report(report):
|
||||||
path = tmp_path / "testModPack" / "reports" / "biomes.md"
|
assert str(report).strip() == textwrap.dedent("""
|
||||||
assert path.read_text() == textwrap.dedent("""
|
|
||||||
# Biome: minecraft:plains (kansascity)
|
# Biome: minecraft:plains (kansascity)
|
||||||
|
|
||||||
* Flora: field
|
* Flora: field
|
||||||
@@ -65,5 +65,4 @@ def test_report(tmp_path, report):
|
|||||||
* Humidity: dry
|
* Humidity: dry
|
||||||
* Soil: sandy
|
* Soil: sandy
|
||||||
* Water: inland
|
* Water: inland
|
||||||
|
""").strip()
|
||||||
""").lstrip()
|
|
||||||
@@ -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 ############################################################################################
|
# 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 mcpacker.model.modpack import ModPack
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
# Class ############################################################################################
|
# Class ############################################################################################
|
||||||
|
|
||||||
class MineralReport(MarkdownWriter):
|
class MineralReport(ReportComposer):
|
||||||
|
|
||||||
def __init__(self, pack:ModPack, outputDir:Path):
|
def doCompose(self):
|
||||||
super().__init__("minerals.md", pack, outputDir)
|
|
||||||
|
|
||||||
def compose(self):
|
|
||||||
for mineral in self.pack.world.minerals:
|
for mineral in self.pack.world.minerals:
|
||||||
self.line(f"# Mineral: {mineral.name}")
|
self.line(f"# Mineral: {mineral.name}")
|
||||||
self.line()
|
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.mineral import Mineral
|
||||||
from mcpacker.model.core.geology.replacement import Replacement
|
from mcpacker.model.core.geology.replacement import Replacement
|
||||||
|
from mcpacker.model.modpack import AugmentFunc
|
||||||
from mcpacker.model.modpack import ModPack
|
from mcpacker.model.modpack import ModPack
|
||||||
from pytest import fixture
|
from pytest import fixture
|
||||||
|
|
||||||
@@ -25,24 +26,18 @@ def defineAddMinerals():
|
|||||||
return addMinerals
|
return addMinerals
|
||||||
|
|
||||||
@fixture(name="pack")
|
@fixture(name="pack")
|
||||||
def createPack(addMinerals):
|
def createPack(addMinerals:AugmentFunc):
|
||||||
pack = ModPack("testModPack")
|
yield ModPack("testModPack").augment(addMinerals)
|
||||||
pack.augment(addMinerals)
|
|
||||||
yield pack
|
|
||||||
|
|
||||||
@fixture(name="report")
|
@fixture(name="report")
|
||||||
def createReport(pack, tmp_path):
|
def createReport(pack:ModPack):
|
||||||
report = MineralReport(pack, tmp_path)
|
yield MineralReport(pack).compose()
|
||||||
report.write()
|
|
||||||
yield report
|
|
||||||
|
|
||||||
|
|
||||||
# Tests ############################################################################################
|
# Tests ############################################################################################
|
||||||
|
|
||||||
def test_report(tmp_path, report):
|
def test_report(report:MineralReport):
|
||||||
path = tmp_path / "testModPack" / "reports" / "minerals.md"
|
assert str(report).strip() == textwrap.dedent("""
|
||||||
|
|
||||||
assert path.read_text() == textwrap.dedent("""
|
|
||||||
# Mineral: copper
|
# Mineral: copper
|
||||||
|
|
||||||
* #minecraft:stone_replaceables => minecraft:copper_ore
|
* #minecraft:stone_replaceables => minecraft:copper_ore
|
||||||
@@ -52,5 +47,4 @@ def test_report(tmp_path, report):
|
|||||||
|
|
||||||
* #minecraft:stone_replaceables => minecraft:iron_ore
|
* #minecraft:stone_replaceables => minecraft:iron_ore
|
||||||
* #minecraft:deepslate_replaceables => minecraft:deepslate_iron_ore
|
* #minecraft:deepslate_replaceables => minecraft:deepslate_iron_ore
|
||||||
|
""").strip()
|
||||||
""").lstrip()
|
|
||||||
+3
-6
@@ -1,17 +1,14 @@
|
|||||||
|
from mcpacker.format.report.composer import ReportComposer
|
||||||
from mcpacker.model.core.ecology.biometrait import BiomeTrait
|
from mcpacker.model.core.ecology.biometrait import BiomeTrait
|
||||||
from mcpacker.model.modpack import ModPack
|
from mcpacker.model.modpack import ModPack
|
||||||
from mcpacker.write.markdown.markdownwriter import MarkdownWriter
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
# Class ############################################################################################
|
# Class ############################################################################################
|
||||||
|
|
||||||
class MobSpawnReport(MarkdownWriter):
|
class MobSpawnReport(ReportComposer):
|
||||||
|
|
||||||
def __init__(self, pack:ModPack, outputDir:Path):
|
def doCompose(self):
|
||||||
super().__init__("mobspawns.md", pack, outputDir)
|
|
||||||
|
|
||||||
def compose(self):
|
|
||||||
for spawn in self.pack.world.mobSpawns:
|
for spawn in self.pack.world.mobSpawns:
|
||||||
self.line(f"# Mob: {spawn.gameId}")
|
self.line(f"# Mob: {spawn.gameId}")
|
||||||
self.line()
|
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.ecology.biomefilter import BiomeFilter as BF
|
||||||
from mcpacker.model.core.fauna.mob import Mob
|
from mcpacker.model.core.fauna.mob import Mob
|
||||||
from mcpacker.model.core.fauna.mobspawn import MobSpawn
|
from mcpacker.model.core.fauna.mobspawn import MobSpawn
|
||||||
from mcpacker.model.core.geology.mineral import Mineral
|
from mcpacker.model.core.geology.mineral import Mineral
|
||||||
from mcpacker.model.core.geology.replacement import Replacement
|
from mcpacker.model.core.geology.replacement import Replacement
|
||||||
from mcpacker.model.core.habitat import Habitat
|
from mcpacker.model.core.habitat import Habitat
|
||||||
|
from mcpacker.model.modpack import AugmentFunc
|
||||||
from mcpacker.model.modpack import ModPack
|
from mcpacker.model.modpack import ModPack
|
||||||
from mcpacker.write.markdown.mobspawnreport import MobSpawnReport
|
|
||||||
from pytest import fixture
|
from pytest import fixture
|
||||||
|
|
||||||
import mcpacker.model.core.altitude as AL
|
import mcpacker.model.core.altitude as AL
|
||||||
@@ -56,23 +57,18 @@ def defineAddMobSpawnss():
|
|||||||
yield addMobSpawns
|
yield addMobSpawns
|
||||||
|
|
||||||
@fixture(name="pack")
|
@fixture(name="pack")
|
||||||
def createPack(addMobs, addMobSpawns):
|
def createPack(addMobs:AugmentFunc, addMobSpawns:AugmentFunc):
|
||||||
pack = ModPack("testModPack")
|
yield ModPack("testModPack").augment(addMobs).augment(addMobSpawns)
|
||||||
pack.augment(addMobs)
|
|
||||||
pack.augment(addMobSpawns)
|
|
||||||
yield pack
|
|
||||||
|
|
||||||
@fixture(name="report")
|
@fixture(name="report")
|
||||||
def createReport(pack, tmp_path):
|
def createReport(pack:ModPack):
|
||||||
report = MobSpawnReport(pack, tmp_path)
|
yield MobSpawnReport(pack).compose()
|
||||||
report.write()
|
|
||||||
yield report
|
|
||||||
|
|
||||||
# Tests ############################################################################################
|
# Tests ############################################################################################
|
||||||
|
|
||||||
def test_write(report, tmp_path):
|
def test_write(report:MobSpawnReport):
|
||||||
path = tmp_path / "testModPack" / "reports" / "mobspawns.md"
|
assert str(report).strip() == textwrap.dedent("""
|
||||||
assert path.read_text() == textwrap.dedent("""
|
|
||||||
# Mob: minecraft:chicken
|
# Mob: minecraft:chicken
|
||||||
|
|
||||||
* habitat 1
|
* habitat 1
|
||||||
@@ -95,5 +91,4 @@ def test_write(report, tmp_path):
|
|||||||
* group: troup
|
* group: troup
|
||||||
* location: outside
|
* location: outside
|
||||||
* scarcity: uncommon
|
* scarcity: uncommon
|
||||||
|
""").strip()
|
||||||
""").lstrip()
|
|
||||||
@@ -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.resourcepack.model import Model
|
||||||
from mcpacker.model.modpack import ModPack
|
from mcpacker.model.modpack import ModPack
|
||||||
from mcpacker.model.core.resourceid import ResourceId
|
from mcpacker.model.core.resourceid import ResourceId
|
||||||
|
from mcpacker.model.datapack.recipe.shapedrecipe import ShapedRecipe
|
||||||
|
|
||||||
|
|
||||||
# Class ############################################################################################
|
# Class ############################################################################################
|
||||||
@@ -23,4 +24,3 @@ class BlockFactory:
|
|||||||
resource.blockStates.append(BlockState(Variant(blockId)))
|
resource.blockStates.append(BlockState(Variant(blockId)))
|
||||||
|
|
||||||
data = self.pack.dataPack.get(gameId.mod)
|
data = self.pack.dataPack.get(gameId.mod)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
|
||||||
# Class ############################################################################################
|
# Class ############################################################################################
|
||||||
|
|
||||||
class Altitude:
|
class Altitude:
|
||||||
@@ -25,6 +28,7 @@ class Altitude:
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"{self.name} <{self.bottom} to {self.top}>"
|
return f"{self.name} <{self.bottom} to {self.top}>"
|
||||||
|
|
||||||
|
|
||||||
# Constants ########################################################################################
|
# Constants ########################################################################################
|
||||||
|
|
||||||
# Aboveground
|
# Aboveground
|
||||||
@@ -61,17 +65,18 @@ OVERGROUND = (DUNES, LOWLANDS, UPLANDS, HILLS, ALPINE, CRAGS, PEAKS, SUMMIT, SK
|
|||||||
UNDERGROUND = (SOIL, SUBSTRATE, OVERBURDEN, CRUST, MANTLE, PLUTONIC)
|
UNDERGROUND = (SOIL, SUBSTRATE, OVERBURDEN, CRUST, MANTLE, PLUTONIC)
|
||||||
UNDERWATER = (SURFACE, SHALLOWS, DEEPS, ABYSS)
|
UNDERWATER = (SURFACE, SHALLOWS, DEEPS, ABYSS)
|
||||||
|
|
||||||
|
|
||||||
# Helper Functions #################################################################################
|
# Helper Functions #################################################################################
|
||||||
|
|
||||||
def span(*altitudes):
|
def span(*altitudes:Altitude):
|
||||||
if not altitudes: return ANYWHERE
|
if not altitudes: return ANYWHERE
|
||||||
result = Altitude("", ANYWHERE.top, ANYWHERE.bottom)
|
result = Altitude("", ANYWHERE.top, ANYWHERE.bottom)
|
||||||
|
|
||||||
bottom = OVERGROUND[-1].top
|
bottom = OVERGROUND[-1].top
|
||||||
bottomAltitude = None
|
bottomAltitude = OVERGROUND[-1]
|
||||||
|
|
||||||
top = UNDERGROUND[-1].bottom
|
top = UNDERGROUND[-1].bottom
|
||||||
topAltitude = None
|
topAltitude = UNDERGROUND[-1]
|
||||||
|
|
||||||
for altitude in altitudes:
|
for altitude in altitudes:
|
||||||
if altitude.bottom < bottom:
|
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.biome import Biome
|
||||||
from mcpacker.model.core.ecology.biometrait import BiomeTrait
|
from mcpacker.model.core.ecology.biometrait import BiomeTrait
|
||||||
|
|
||||||
@@ -26,8 +26,8 @@ class BiomeFilter:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
required:Iterable[BiomeTrait|Iterable[BiomeTrait]]|None=None,
|
required:Sequence[BiomeTrait|Sequence[BiomeTrait]]|None=None,
|
||||||
prohibited:Iterable[BiomeTrait]|None=None,
|
prohibited:Sequence[BiomeTrait]|None=None,
|
||||||
):
|
):
|
||||||
self.required = required or []
|
self.required = required or []
|
||||||
self.prohibited = prohibited or []
|
self.prohibited = prohibited or []
|
||||||
|
|||||||
@@ -39,14 +39,14 @@ ALL = [SOLO, PAIR, FAMILY, TROUP, HERD]
|
|||||||
|
|
||||||
# Helper Functions #################################################################################
|
# Helper Functions #################################################################################
|
||||||
|
|
||||||
def merge(*groups):
|
def merge(*groups:Group):
|
||||||
if not groups: return SOLO
|
if not groups: return SOLO
|
||||||
|
|
||||||
smallest = ALL[-1].largest
|
smallest = ALL[-1].largest
|
||||||
smallestGroup = None
|
smallestGroup = ALL[-1]
|
||||||
|
|
||||||
largest = ALL[0].smallest
|
largest = ALL[0].smallest
|
||||||
largestGroup = None
|
largestGroup = ALL[0]
|
||||||
|
|
||||||
for group in groups:
|
for group in groups:
|
||||||
if group.smallest < smallest:
|
if group.smallest < smallest:
|
||||||
|
|||||||
@@ -4,19 +4,20 @@ from mcpacker.model.core.fauna.mobspawn import MobSpawn
|
|||||||
from mcpacker.model.core.habitat import Habitat
|
from mcpacker.model.core.habitat import Habitat
|
||||||
from pytest import fixture
|
from pytest import fixture
|
||||||
|
|
||||||
import mcpacker.model.core.ecology.flora as F
|
import mcpacker.model.core.fauna.active as AC
|
||||||
import mcpacker.model.core.ecology.heat as E
|
import mcpacker.model.core.ecology.flora as FL
|
||||||
|
import mcpacker.model.core.ecology.heat as HE
|
||||||
|
|
||||||
|
|
||||||
# Fixtures #########################################################################################
|
# Fixtures #########################################################################################
|
||||||
|
|
||||||
@fixture(name="cow")
|
@fixture(name="cow")
|
||||||
def createCow():
|
def createCow():
|
||||||
yield Mob("cow", "minecraft:cow")
|
yield Mob("minecraft:cow", AC.DIURNAL)
|
||||||
|
|
||||||
@fixture(name="fields")
|
@fixture(name="fields")
|
||||||
def createFieldsHabitat():
|
def createFieldsHabitat():
|
||||||
yield Habitat(biomeFilter=BiomeFilter([F.FIELD, E.TEMPERATE]))
|
yield Habitat(biomeFilter=BiomeFilter([FL.FIELD, HE.TEMPERATE]))
|
||||||
|
|
||||||
@fixture(name="spawn")
|
@fixture(name="spawn")
|
||||||
def createMobSpawn(cow, fields):
|
def createMobSpawn(cow, fields):
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class Habitat:
|
|||||||
self.location = location
|
self.location = location
|
||||||
self.scarcity = scarcity
|
self.scarcity = scarcity
|
||||||
|
|
||||||
self._source = None
|
self._source:Habitat|None = None
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return "".join([str(p) for p in [
|
return "".join([str(p) for p in [
|
||||||
@@ -63,17 +63,24 @@ class Habitat:
|
|||||||
"}"
|
"}"
|
||||||
]])
|
]])
|
||||||
|
|
||||||
def derive(self, **kwargs):
|
def derive(
|
||||||
kwargs = {
|
self,
|
||||||
"altitude": self.altitude,
|
*,
|
||||||
"biomeFilter": self.biomeFilter,
|
altitude:Altitude|None=None,
|
||||||
"seasons": self.seasons,
|
biomeFilter:BiomeFilter|None=None,
|
||||||
"group": self.group,
|
seasons:Iterable[Season]|Season|None=None,
|
||||||
"location": self.location,
|
group:Group|None=None,
|
||||||
"scarcity": self.scarcity
|
location:Location|None=None,
|
||||||
} | kwargs
|
scarcity:Scarcity|None=None,
|
||||||
|
):
|
||||||
result = Habitat(**kwargs)
|
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
|
result._source = self
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ from mcpacker.model.datapack.moddata import ModData
|
|||||||
# Class ############################################################################################
|
# Class ############################################################################################
|
||||||
|
|
||||||
class DataPack:
|
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):
|
def __init__(self, name:str, mods:Iterable[ModData]|None=None):
|
||||||
self.name = name
|
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.mod import Mod
|
||||||
from mcpacker.model.resourcepack import ResourcePack
|
from mcpacker.model.resourcepack import ResourcePack
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
|
|
||||||
|
# Type Helpers #####################################################################################
|
||||||
|
|
||||||
|
type AugmentFunc = Callable[[ModPack], None]
|
||||||
|
|
||||||
|
|
||||||
# Class ############################################################################################
|
# Class ############################################################################################
|
||||||
@@ -27,6 +33,6 @@ class ModPack:
|
|||||||
for mod in (mods or []):
|
for mod in (mods or []):
|
||||||
self._mods[mod.name] = mod
|
self._mods[mod.name] = mod
|
||||||
|
|
||||||
def augment(self, doAugment:"Callable[[ModPack], None]") -> "ModPack":
|
def augment(self, doAugment:AugmentFunc) -> Self:
|
||||||
doAugment(self)
|
doAugment(self)
|
||||||
return 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.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.compositewriter import CompositeWriter
|
||||||
|
from mcpacker.write.incontrol.spawnerwriter import SpawnerWriter
|
||||||
|
from mcpacker.write.report.writer import ReportWriter
|
||||||
from mcpacker.write.staticwriter import StaticWriter
|
from mcpacker.write.staticwriter import StaticWriter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
@@ -46,13 +45,13 @@ class Runner:
|
|||||||
|
|
||||||
def _command_writeReports(self):
|
def _command_writeReports(self):
|
||||||
CompositeWriter(self.pack, self.outputDir, [
|
CompositeWriter(self.pack, self.outputDir, [
|
||||||
BiomeReport,
|
ReportWriter(self.pack, BiomeReport, self.outputDir, "biomes.md"),
|
||||||
MineralReport,
|
ReportWriter(self.pack, MineralReport, self.outputDir, "minerals.md"),
|
||||||
MobSpawnReport,
|
ReportWriter(self.pack, MobSpawnReport, self.outputDir, "mobspawns.md"),
|
||||||
]).write()
|
]).write()
|
||||||
|
|
||||||
def _command_writeModPack(self):
|
def _command_writeModPack(self):
|
||||||
CompositeWriter(self.pack, self.outputDir, [
|
CompositeWriter(self.pack, self.outputDir, [
|
||||||
StaticWriter,
|
StaticWriter(self.pack, self.outputDir),
|
||||||
SpawnerWriter,
|
SpawnerWriter(self.pack, self.outputDir),
|
||||||
]).write()
|
]).write()
|
||||||
|
|||||||
@@ -2,15 +2,6 @@ from collections.abc import Iterable
|
|||||||
from mcpacker.model.modpack import ModPack
|
from mcpacker.model.modpack import ModPack
|
||||||
from mcpacker.write.writer import Writer
|
from mcpacker.write.writer import Writer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TypeVar
|
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
|
|
||||||
# Type Support #####################################################################################
|
|
||||||
|
|
||||||
WriterSubclass = TypeVar("WriterSubclass", bound=Writer)
|
|
||||||
|
|
||||||
|
|
||||||
# Class ############################################################################################
|
# Class ############################################################################################
|
||||||
@@ -21,16 +12,10 @@ class CompositeWriter(Writer):
|
|||||||
self,
|
self,
|
||||||
pack:ModPack,
|
pack:ModPack,
|
||||||
outputDir:Path,
|
outputDir:Path,
|
||||||
writers:Iterable[type[WriterSubclass]]|None=None
|
writers:Iterable[Writer]|None=None
|
||||||
):
|
):
|
||||||
super().__init__(pack, outputDir)
|
super().__init__(pack, outputDir)
|
||||||
|
self._writers:list[Writer] = list(w for w in writers or [])
|
||||||
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))
|
|
||||||
|
|
||||||
def write(self):
|
def write(self):
|
||||||
for writer in self._writers:
|
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 mcpacker.model.modpack import ModPack
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@@ -18,5 +19,5 @@ class Writer:
|
|||||||
os.remove(file)
|
os.remove(file)
|
||||||
file.parent.mkdir(parents=True, exist_ok=True)
|
file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
def write(self):
|
def write(self) -> Self:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|||||||
Reference in New Issue
Block a user