1
0
mirror of synced 2024-12-12 15:01:09 +01:00

[malody] Dumping does not write placeholder null values anymore

This commit is contained in:
Stepland 2021-06-01 19:06:53 +02:00
parent 8021e10c2e
commit a932893edc
6 changed files with 118 additions and 38 deletions

View File

@ -1,3 +1,7 @@
# v1.2.2
## Fixed
- [malody] Dumping does not write placeholder `null` values anymore
# v1.2.1
## Fixed
- [malody] Parsing a file with keys that are unused for conversion

View File

@ -12,7 +12,26 @@ from jubeatools.testutils.typing import DrawFunc
@st.composite
def memo_compatible_metadata(draw: DrawFunc) -> song.Metadata:
text_strat = st.text(alphabet=st.characters(min_codepoint=0x20, max_codepoint=0x7E))
# some ranges that are valid in shift-jis
text_strat = st.text(
alphabet=st.one_of(
*(
st.characters(min_codepoint=a, max_codepoint=b)
for a, b in (
(0x20, 0x7F),
(0xB6, 0x109),
(0x410, 0x44F),
(0x24D0, 0x24E9),
(0x3041, 0x3096),
(0x309B, 0x30FF),
(0xFA30, 0xFA6A),
(0xFF01, 0xFF3B),
(0xFF3D, 0xFF5D),
(0xFF61, 0xFF9F),
)
)
)
)
metadata: song.Metadata = draw(
jbst.metadata(text_strat=text_strat, path_strat=text_strat)
)

View File

@ -1,7 +1,7 @@
import time
from functools import singledispatch
from pathlib import Path
from typing import List, Tuple, Union
from typing import List, Optional, Tuple, Union
import simplejson as json
@ -30,7 +30,7 @@ dump_malody = make_dumper_from_chart_file_dumper(
def dump_malody_chart(
metadata: song.Metadata, dif: str, chart: song.Chart, timing: song.Timing
metadata: song.Metadata, dif: Optional[str], chart: song.Chart, timing: song.Timing
) -> malody.Chart:
meta = dump_metadata(metadata, dif)
time = dump_timing(timing)
@ -40,10 +40,10 @@ def dump_malody_chart(
return malody.Chart(meta=meta, time=time, note=notes)
def dump_metadata(metadata: song.Metadata, dif: str) -> malody.Metadata:
def dump_metadata(metadata: song.Metadata, dif: Optional[str]) -> malody.Metadata:
return malody.Metadata(
cover="",
creator="",
cover=None,
creator=None,
background=none_or(str, metadata.cover),
version=dif,
id=0,

View File

@ -1,21 +1,15 @@
from dataclasses import dataclass, field
from decimal import Decimal
from enum import Enum
from typing import List, Optional, Tuple, Union
from typing import Any, List, Optional, Tuple, Union
from marshmallow import EXCLUDE
from marshmallow import EXCLUDE, Schema, post_dump
from marshmallow.validate import Range
from marshmallow_dataclass import NewType, class_schema
class Ordered:
class Meta:
ordered = True
unknown = EXCLUDE
@dataclass
class SongInfo(Ordered):
class SongInfo:
title: Optional[str]
artist: Optional[str]
id: Optional[int]
@ -32,7 +26,7 @@ class Mode(int, Enum):
@dataclass
class Metadata(Ordered):
class Metadata:
cover: Optional[str] # path to album art ?
creator: Optional[str] # Chart author
background: Optional[str] # path to background image
@ -51,7 +45,7 @@ StrictlyPositiveDecimal = NewType(
@dataclass
class BPMEvent(Ordered):
class BPMEvent:
beat: BeatTime
bpm: StrictlyPositiveDecimal
@ -60,13 +54,13 @@ ButtonIndex = NewType("ButtonIndex", int, validate=Range(min=0, max=15))
@dataclass
class TapNote(Ordered):
class TapNote:
beat: BeatTime
index: ButtonIndex
@dataclass
class LongNote(Ordered):
class LongNote:
beat: BeatTime
index: ButtonIndex
endbeat: BeatTime
@ -74,7 +68,7 @@ class LongNote(Ordered):
@dataclass
class Sound(Ordered):
class Sound:
"""Used both for the background music and keysounds"""
beat: BeatTime
@ -95,10 +89,20 @@ Event = Union[Sound, LongNote, TapNote]
@dataclass
class Chart(Ordered):
class Chart:
meta: Metadata
time: List[BPMEvent] = field(default_factory=list)
note: List[Event] = field(default_factory=list)
CHART_SCHEMA = class_schema(Chart)()
class BaseSchema(Schema):
class Meta:
ordered = True
unknown = EXCLUDE
@post_dump
def remove_none_values(self, data: dict, **kwargs: Any) -> dict:
return {key: value for key, value in data.items() if value is not None}
CHART_SCHEMA = class_schema(Chart, base_schema=BaseSchema)()

View File

@ -1,32 +1,85 @@
from dataclasses import fields
from decimal import Decimal
from typing import Optional
import simplejson as json
from hypothesis import given
from hypothesis import strategies as st
from jubeatools import song
from jubeatools.formats import Format
from jubeatools.formats.konami.testutils import open_temp_dir
from jubeatools.formats.malody import schema as malody
from jubeatools.formats.malody.dump import dump_malody_chart
from jubeatools.testutils import strategies as jbst
from jubeatools.testutils.test_patterns import dump_and_load_then_compare
from jubeatools.testutils.typing import DrawFunc
@st.composite
def malody_compatible_song(draw: DrawFunc) -> song.Song:
"""Malody files only hold one chart and have limited metadata"""
diff = draw(st.sampled_from(list(song.Difficulty))).value
chart = draw(jbst.chart(level_strat=st.just(Decimal(0))))
metadata = draw(jbst.metadata())
def difficulty(draw: DrawFunc) -> str:
d: song.Difficulty = draw(st.sampled_from(list(song.Difficulty)))
return d.value
@st.composite
def chart(draw: DrawFunc) -> song.Chart:
c: song.Chart = draw(jbst.chart(level_strat=st.just(Decimal(0))))
return c
@st.composite
def metadata(draw: DrawFunc) -> song.Metadata:
metadata: song.Metadata = draw(jbst.metadata())
metadata.preview = None
metadata.preview_file = None
return song.Song(metadata=metadata, charts={diff: chart})
return metadata
@given(malody_compatible_song())
def test_that_full_chart_roundtrips(song: song.Song) -> None:
@st.composite
def malody_song(draw: DrawFunc) -> song.Song:
"""Malody files only hold one chart and have limited metadata"""
diff = draw(difficulty())
chart_ = draw(chart())
metadata_ = draw(metadata())
return song.Song(metadata=metadata_, charts={diff: chart_})
@given(malody_song())
def test_that_full_chart_roundtrips(s: song.Song) -> None:
dump_and_load_then_compare(
Format.MALODY,
song,
s,
temp_path=open_temp_dir(),
bytes_decoder=lambda b: b.decode("utf-8"),
)
@given(chart(), metadata(), st.one_of(st.none(), difficulty()))
def test_that_none_values_in_metadata_dont_appear_in_dumped_json(
chart: song.Chart,
metadata: song.Metadata,
dif: Optional[str],
) -> None:
assert chart.timing is not None
malody_chart = dump_malody_chart(metadata, dif, chart, chart.timing)
json_chart = malody.CHART_SCHEMA.dump(malody_chart)
assert all(value is not None for value in json_chart["meta"].values())
@given(malody_song())
def test_that_field_are_ordered(s: song.Song) -> None:
dif, chart = next(iter(s.charts.items()))
assert chart.timing is not None
malody_chart = dump_malody_chart(s.metadata, dif, chart, chart.timing)
json_chart = malody.CHART_SCHEMA.dump(malody_chart)
text_chart = json.dumps(json_chart, indent=4, use_decimal=True)
reparsed_chart = json.loads(
text_chart,
)
# dict is ordered in 3.8 ... right ?
order_in_file = list(reparsed_chart["meta"].keys())
order_in_definition = list(
f.name for f in fields(malody.Metadata) if f.name in reparsed_chart["meta"]
)
assert order_in_file == order_in_definition

View File

@ -21,11 +21,11 @@ def dump_and_load_then_compare(
dump_options = dump_options or {}
loader = LOADERS[format_]
dumper = DUMPERS[format_]
with temp_path as path:
files = dumper(song, path, **dump_options)
for path, bytes_ in files.items():
path.write_bytes(bytes_)
note(f"Wrote to {path} :\n{bytes_decoder(bytes_)}")
assert guess_format(path) == format_
recovered_song = loader(path, **load_options)
with temp_path as folder_path:
files = dumper(song, folder_path, **dump_options)
for file_path, bytes_ in files.items():
file_path.write_bytes(bytes_)
note(f"Wrote to {file_path} :\n{bytes_decoder(bytes_)}")
assert guess_format(file_path) == format_
recovered_song = loader(folder_path, **load_options)
assert recovered_song == song