From 979b0e648f79ced29b29aa05f99025338a244824 Mon Sep 17 00:00:00 2001 From: Stepland <16676308+Stepland@users.noreply.github.com> Date: Thu, 29 Apr 2021 00:27:58 +0200 Subject: [PATCH] Switch to pathlib + mypy stuff --- .gitignore | 1 + jubeatools/cli.py | 2 +- jubeatools/formats/__init__.py | 4 +- jubeatools/formats/guess.py | 4 +- .../formats/jubeat_analyser/dump_tools.py | 28 ++++++---- jubeatools/formats/jubeat_analyser/files.py | 12 ++--- .../formats/jubeat_analyser/load_tools.py | 6 +-- .../formats/jubeat_analyser/memo/dump.py | 10 ++-- .../formats/jubeat_analyser/memo/load.py | 19 +++++-- .../formats/jubeat_analyser/memo1/dump.py | 15 +++--- .../formats/jubeat_analyser/memo1/load.py | 15 +++--- .../formats/jubeat_analyser/memo2/dump.py | 54 ++++++++++--------- .../formats/jubeat_analyser/memo2/load.py | 40 +++++++------- .../jubeat_analyser/mono_column/dump.py | 8 +-- .../jubeat_analyser/mono_column/load.py | 12 +++-- jubeatools/formats/memon/__init__.py | 24 ++++----- jubeatools/formats/memon/test_memon.py | 2 +- jubeatools/formats/typing.py | 10 ++-- jubeatools/song.py | 4 +- jubeatools/testutils/strategies.py | 2 +- 20 files changed, 149 insertions(+), 123 deletions(-) diff --git a/.gitignore b/.gitignore index 61eebf3..92f4609 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .vscode .hypothesis .pytest_cache +.mypy_cache dist \ No newline at end of file diff --git a/jubeatools/cli.py b/jubeatools/cli.py index b4c3a47..1986f1e 100644 --- a/jubeatools/cli.py +++ b/jubeatools/cli.py @@ -2,7 +2,7 @@ from typing import Optional import click -from path import Path +from pathlib import Path from jubeatools.formats import DUMPERS, LOADERS from jubeatools.formats.enum import JUBEAT_ANALYSER_FORMATS diff --git a/jubeatools/formats/__init__.py b/jubeatools/formats/__init__.py index 63a4fb5..14801fb 100644 --- a/jubeatools/formats/__init__.py +++ b/jubeatools/formats/__init__.py @@ -4,7 +4,7 @@ Module containing all the load/dump code for all file formats from typing import IO, Any, Callable, Dict -from path import Path +from pathlib import Path from jubeatools.song import Song @@ -41,7 +41,7 @@ LOADERS: Dict[Format, Callable[[Path], Song]] = { Format.MEMO_2: load_memo2, } -DUMPERS: Dict[str, Dumper] = { +DUMPERS: Dict[Format, Dumper] = { Format.MEMON_LEGACY: dump_memon_legacy, Format.MEMON_0_1_0: dump_memon_0_1_0, Format.MEMON_0_2_0: dump_memon_0_2_0, diff --git a/jubeatools/formats/guess.py b/jubeatools/formats/guess.py index 454976f..cd70442 100644 --- a/jubeatools/formats/guess.py +++ b/jubeatools/formats/guess.py @@ -2,13 +2,13 @@ import json import re from typing import Any, List -from path import Path +from pathlib import Path from .enum import Format def guess_format(path: Path) -> Format: - if path.isdir(): + if path.is_dir(): raise ValueError("Can't guess chart format for a folder") # The file is valid json => memon diff --git a/jubeatools/formats/jubeat_analyser/dump_tools.py b/jubeatools/formats/jubeat_analyser/dump_tools.py index a81695a..ad7978f 100644 --- a/jubeatools/formats/jubeat_analyser/dump_tools.py +++ b/jubeatools/formats/jubeat_analyser/dump_tools.py @@ -5,10 +5,10 @@ from dataclasses import dataclass, field from decimal import Decimal from fractions import Fraction from itertools import chain -from typing import Callable, Dict, Iterator, List, Mapping, Optional, Union +from typing import Callable, Dict, Iterator, List, Mapping, Optional, Union, TypeVar from more_itertools import collapse, intersperse, mark_ends, windowed -from path import Path +from pathlib import Path from sortedcontainers import SortedDict, SortedKeyList from jubeatools.formats.filetypes import ChartFile @@ -111,15 +111,19 @@ class SortedDefaultDict(SortedDict): return value +# Here we split dataclass and ABC stuff since mypy curently can't handle both +# at once on a single class definition @dataclass -class JubeatAnalyserDumpedSection(ABC): +class _JubeatAnalyerDumpedSection: current_beat: BeatsTime - length: Decimal = 4 + length: Decimal = Decimal(4) commands: Dict[str, Optional[str]] = field(default_factory=dict) symbol_definitions: Dict[BeatsTime, str] = field(default_factory=dict) symbols: Dict[BeatsTime, str] = field(default_factory=dict) notes: List[Union[TapNote, LongNote, LongNoteEnd]] = field(default_factory=list) + +class JubeatAnalyserDumpedSection(_JubeatAnalyerDumpedSection, ABC): def _dump_commands(self) -> Iterator[str]: keys = chain(COMMAND_ORDER, self.commands.keys() - set(COMMAND_ORDER)) for key in keys: @@ -139,14 +143,16 @@ class JubeatAnalyserDumpedSection(ABC): ... +S = TypeVar('S', bound=JubeatAnalyserDumpedSection) + def create_sections_from_chart( - section_factory: Callable[[BeatsTime], JubeatAnalyserDumpedSection], + section_factory: Callable[[BeatsTime], S], chart: Chart, difficulty: str, timing: Timing, metadata: Metadata, circle_free: bool, -) -> Mapping[BeatsTime, JubeatAnalyserDumpedSection]: +) -> Mapping[BeatsTime, S]: sections = SortedDefaultDict(section_factory) timing_events = sorted(timing.events, key=lambda e: e.time) @@ -225,7 +231,7 @@ def jubeat_analyser_file_dumper( ) -> Dict[Path, bytes]: files = internal_dumper(song, circle_free) res = {} - if path.isdir(): + if path.is_dir(): name_format = song.metadata.title + " {difficulty}{dedup_index}.txt" else: name_format = "{base}{dedup_index}{ext}" @@ -233,18 +239,18 @@ def jubeat_analyser_file_dumper( for chartfile in files: i = 0 filepath = name_format.format( - base=path.stripext(), + base=path.parent / path.stem, difficulty = DIFFICULTIES.get(chartfile.difficulty, chartfile.difficulty), dedup_index = "" if i == 0 else f"-{i}", - ext=path.ext, + ext=path.suffix, ) while filepath in res: i += 1 filepath = name_format.format( - base=path.stripext(), + base=path.parent / path.stem, difficulty = DIFFICULTIES.get(chartfile.difficulty, chartfile.difficulty), dedup_index = "" if i == 0 else f"-{i}", - ext=path.ext, + ext=path.suffix, ) res[Path(filepath)] = chartfile.contents.getvalue().encode( diff --git a/jubeatools/formats/jubeat_analyser/files.py b/jubeatools/formats/jubeat_analyser/files.py index 1958d87..117bec2 100644 --- a/jubeatools/formats/jubeat_analyser/files.py +++ b/jubeatools/formats/jubeat_analyser/files.py @@ -1,23 +1,23 @@ from typing import Dict, List -from path import Path +from pathlib import Path def load_files(path: Path) -> Dict[Path, List[str]]: # The vast majority of memo files you will encounter will be propely # decoded using shift-jis-2004. Get ready for endless fun with the small # portion of files that won't - files = {} - if path.isdir(): - for f in path.files("*.txt"): + files: Dict[Path, List[str]] = {} + if path.is_dir(): + for f in path.glob("*.txt"): _load_file(f, files) - elif path.isfile(): + elif path.is_file(): _load_file(path, files) return files def _load_file(path: Path, files: Dict[Path, List[str]]): try: - files[path] = path.lines("shift-jis-2004") + files[path] = path.read_text(encoding="shift-jis-2004").split("\n") except UnicodeDecodeError: pass diff --git a/jubeatools/formats/jubeat_analyser/load_tools.py b/jubeatools/formats/jubeat_analyser/load_tools.py index c4d2ff6..114f6d9 100644 --- a/jubeatools/formats/jubeat_analyser/load_tools.py +++ b/jubeatools/formats/jubeat_analyser/load_tools.py @@ -6,7 +6,7 @@ from copy import deepcopy from dataclasses import astuple, dataclass from decimal import Decimal from itertools import product, zip_longest -from typing import Dict, Iterator, List, Optional, Set, Tuple +from typing import Dict, Iterator, List, Optional, Set, Tuple, AbstractSet import constraint from parsimonious import Grammar, NodeVisitor, ParseError @@ -164,7 +164,7 @@ class UnfinishedLongNote: def find_long_note_candidates( - bloc: List[List[str]], note_symbols: Set[str], should_skip: Set[NotePosition] + bloc: List[List[str]], note_symbols: AbstractSet[str], should_skip: AbstractSet[NotePosition] ) -> Dict[NotePosition, Set[NotePosition]]: "Return a dict of arrow position to landing note candidates" arrow_to_note_candidates: Dict[NotePosition, Set[NotePosition]] = {} @@ -178,7 +178,7 @@ def find_long_note_candidates( # at this point we are sure we have a long arrow # we need to check in its direction for note candidates - note_candidates: Set[Tuple[int, int]] = set() + note_candidates = set() 𝛿pos = LONG_DIRECTION[symbol] candidate = NotePosition(x, y) + 𝛿pos while 0 <= candidate.x < 4 and 0 <= candidate.y < 4: diff --git a/jubeatools/formats/jubeat_analyser/memo/dump.py b/jubeatools/formats/jubeat_analyser/memo/dump.py index 40784d4..089b916 100644 --- a/jubeatools/formats/jubeat_analyser/memo/dump.py +++ b/jubeatools/formats/jubeat_analyser/memo/dump.py @@ -7,10 +7,10 @@ from functools import partial from io import StringIO from itertools import chain, zip_longest from math import ceil -from typing import Dict, Iterator, List, Optional, Set, Tuple, Union +from typing import Dict, Iterator, List, Optional, Set, Tuple, Union, cast, Callable from more_itertools import chunked, collapse, intersperse, mark_ends, windowed -from path import Path +from pathlib import Path from sortedcontainers import SortedKeyList from jubeatools import __version__ @@ -221,7 +221,7 @@ def _dump_memo_chart( ) -> StringIO: _raise_if_unfit_for_memo(chart, timing, circle_free) - + sections = create_sections_from_chart( MemoDumpedSection, chart, difficulty, timing, metadata, circle_free ) @@ -259,11 +259,13 @@ def _dump_memo_chart( def _dump_memo_internal(song: Song, circle_free: bool) -> List[ChartFile]: files: List[ChartFile] = [] for difficulty, chart in song.charts.items(): + timing = chart.timing or song.global_timing + assert timing is not None contents = _dump_memo_chart( difficulty, chart, song.metadata, - chart.timing or song.global_timing, + timing, circle_free, ) files.append(ChartFile(contents, song, difficulty, chart)) diff --git a/jubeatools/formats/jubeat_analyser/memo/load.py b/jubeatools/formats/jubeat_analyser/memo/load.py index 61ab32a..165203f 100644 --- a/jubeatools/formats/jubeat_analyser/memo/load.py +++ b/jubeatools/formats/jubeat_analyser/memo/load.py @@ -10,7 +10,7 @@ from typing import Dict, Iterator, List, Mapping, Optional, Set, Tuple, Union import constraint from more_itertools import collapse, mark_ends from parsimonious import Grammar, NodeVisitor, ParseError -from path import Path +from pathlib import Path from jubeatools.song import ( Chart, @@ -21,6 +21,7 @@ from jubeatools.song import ( Song, TapNote, Timing, + Preview, ) from ..command import is_command, parse_command @@ -118,7 +119,10 @@ class MemoParser(JubeatAnalyserParser): return list(line) def _frames_duration(self) -> Decimal: - return sum(frame.duration for frame in self.frames) + return sum( + (frame.duration for frame in self.frames), + start=Decimal(0) + ) def _push_frame(self): position_part = [ @@ -198,7 +202,10 @@ class MemoParser(JubeatAnalyserParser): frame_starting_beat = Decimal(0) for i, frame in enumerate(section.frames): if frame.timing_part: - frame_starting_beat = sum(f.duration for f in section.frames[:i]) + frame_starting_beat = sum( + (f.duration for f in section.frames[:i]), + start=Decimal(0) + ) local_symbols = { symbol: Decimal("0.25") * i + frame_starting_beat for i, symbol in enumerate(collapse(frame.timing_part)) @@ -315,8 +322,10 @@ def _load_memo_file(lines: List[str]) -> Song: cover=parser.jacket, ) if parser.preview_start is not None: - metadata.preview_start = SecondsTime(parser.preview_start) / 1000 - metadata.preview_length = SecondsTime(10) + metadata.preview = Preview( + start=SecondsTime(parser.preview_start) / 1000, + length=SecondsTime(10) + ) timing = Timing( events=parser.timing_events, beat_zero_offset=SecondsTime(parser.offset) / 1000 diff --git a/jubeatools/formats/jubeat_analyser/memo1/dump.py b/jubeatools/formats/jubeat_analyser/memo1/dump.py index 08949b6..c062a3a 100644 --- a/jubeatools/formats/jubeat_analyser/memo1/dump.py +++ b/jubeatools/formats/jubeat_analyser/memo1/dump.py @@ -10,7 +10,7 @@ from math import ceil from typing import Dict, Iterator, List, Optional, Set, Tuple, Union from more_itertools import chunked, collapse, intersperse, mark_ends, windowed -from path import Path +from pathlib import Path from sortedcontainers import SortedKeyList from jubeatools import __version__ @@ -58,8 +58,9 @@ class Frame: def dump(self, length: Decimal) -> Iterator[str]: # Check that bars are contiguous for a, b in windowed(sorted(self.bars), 2): - if b is not None and b - a != 1: - raise ValueError("Frame has discontinuous bars") + if a is not None and b is not None: + if b - a != 1: + raise ValueError("Frame has discontinuous bars") # Check all bars are in the same 4-bar group if self.bars.keys() != set(bar % 4 for bar in self.bars): raise ValueError("Frame contains bars from different 4-bar groups") @@ -104,7 +105,7 @@ class Memo1DumpedSection(JubeatAnalyserDumpedSection): notes_by_bar[bar_index].append(note) # Pre-render timing bars - bars: Dict[int, List[str]] = defaultdict(dict) + bars: Dict[int, List[str]] = defaultdict(list) chosen_symbols: Dict[BeatsTime, str] = {} symbols_iterator = iter(NOTE_SYMBOLS) for bar_index in range(ceil(self.length)): @@ -227,7 +228,7 @@ def _dump_memo1_chart( ) # Jubeat Analyser format command - sections[0].commands["memo1"] = None + sections[BeatsTime(0)].commands["memo1"] = None # Actual output to file file = StringIO() @@ -242,11 +243,13 @@ def _dump_memo1_chart( def _dump_memo1_internal(song: Song, circle_free: bool) -> List[ChartFile]: files: List[ChartFile] = [] for difficulty, chart in song.charts.items(): + timing = chart.timing or song.global_timing + assert timing is not None contents = _dump_memo1_chart( difficulty, chart, song.metadata, - chart.timing or song.global_timing, + timing, circle_free, ) files.append(ChartFile(contents, song, difficulty, chart)) diff --git a/jubeatools/formats/jubeat_analyser/memo1/load.py b/jubeatools/formats/jubeat_analyser/memo1/load.py index 6cf924a..9929c8d 100644 --- a/jubeatools/formats/jubeat_analyser/memo1/load.py +++ b/jubeatools/formats/jubeat_analyser/memo1/load.py @@ -10,7 +10,7 @@ from typing import Dict, Iterator, List, Mapping, Optional, Set, Tuple, Union import constraint from more_itertools import collapse, mark_ends from parsimonious import Grammar, NodeVisitor, ParseError -from path import Path +from pathlib import Path from jubeatools.song import ( BeatsTime, @@ -22,6 +22,7 @@ from jubeatools.song import ( Song, TapNote, Timing, + Preview ) from ..command import is_command, parse_command @@ -112,7 +113,7 @@ class Memo1Parser(JubeatAnalyserParser): return list(line) def _frames_duration(self) -> Decimal: - return sum(frame.duration for frame in self.frames) + return sum((frame.duration for frame in self.frames), start=Decimal(0)) def _push_frame(self): position_part = [ @@ -184,13 +185,13 @@ class Memo1Parser(JubeatAnalyserParser): ]: """iterate over tuples of currently_defined_symbols, frame, section_starting_beat, section""" - local_symbols: Dict[str, Decimal] = {} + local_symbols = {} section_starting_beat = Decimal(0) for section in self.sections: frame_starting_beat = Decimal(0) for i, frame in enumerate(section.frames): if frame.timing_part: - frame_starting_beat = sum(f.duration for f in section.frames[:i]) + frame_starting_beat = sum((f.duration for f in section.frames[:i]), start=Decimal(0)) local_symbols = { symbol: BeatsTime(symbol_index, len(bar)) + bar_index @@ -309,8 +310,10 @@ def _load_memo1_file(lines: List[str]) -> Song: cover=parser.jacket, ) if parser.preview_start is not None: - metadata.preview_start = SecondsTime(parser.preview_start) / 1000 - metadata.preview_length = SecondsTime(10) + metadata.preview = Preview( + start=SecondsTime(parser.preview_start) / 1000, + length=SecondsTime(10) + ) timing = Timing( events=parser.timing_events, beat_zero_offset=SecondsTime(parser.offset) / 1000 diff --git a/jubeatools/formats/jubeat_analyser/memo2/dump.py b/jubeatools/formats/jubeat_analyser/memo2/dump.py index 78d7f59..d6a39e1 100644 --- a/jubeatools/formats/jubeat_analyser/memo2/dump.py +++ b/jubeatools/formats/jubeat_analyser/memo2/dump.py @@ -10,7 +10,7 @@ from math import ceil from typing import Dict, Iterator, List, Optional, Set, Tuple, Union from more_itertools import chunked, collapse, intersperse, mark_ends, windowed -from path import Path +from pathlib import Path from sortedcontainers import SortedKeyList from jubeatools import __version__ @@ -38,7 +38,6 @@ from ..dump_tools import ( DIRECTION_TO_ARROW, DIRECTION_TO_LINE, NOTE_TO_CIRCLE_FREE_SYMBOL, - JubeatAnalyserDumpedSection, LongNoteEnd, SortedDefaultDict, create_sections_from_chart, @@ -61,8 +60,9 @@ class Frame: def dump(self) -> Iterator[str]: # Check that bars are contiguous for a, b in windowed(sorted(self.bars), 2): - if b is not None and b - a != 1: - raise ValueError("Frame has discontinuous bars") + if a is not None and b is not None: + if b - a != 1: + raise ValueError("Frame has discontinuous bars") for pos, bar in zip_longest(self.dump_positions(), self.dump_bars()): if bar is None: @@ -91,6 +91,13 @@ class StopEvent: duration: SecondsTime +@dataclass +class BarEvent: + note: Optional[str] = None + bpms: List[BPMEvent] = field(default_factory=list) + stops: List[StopEvent] = field(default_factory=list) + + @dataclass class Memo2Section: """A 4-beat-long group of notes""" @@ -115,7 +122,7 @@ class Memo2Section: events_by_bar[bar_index].append(event) # Pre-render timing bars - bars: Dict[int, List[str]] = defaultdict(dict) + bars: Dict[int, List[str]] = defaultdict(list) chosen_symbols: Dict[BeatsTime, str] = {} symbols_iterator = iter(NOTE_SYMBOLS) for bar_index in range(4): @@ -127,9 +134,8 @@ class Memo2Section: ) if bar_length < 3: bar_length = 4 - bar_dict: Dict[int, List[Union[str, BPMEvent, StopEvent]]] = defaultdict( - list - ) + + bar_dict: Dict[int, BarEvent] = defaultdict(BarEvent) for note in notes: time_in_section = note.time % BeatsTime(4) time_in_bar = note.time % Fraction(1) @@ -139,34 +145,28 @@ class Memo2Section: if time_index not in bar_dict: symbol = next(symbols_iterator) chosen_symbols[time_in_section] = symbol - bar_dict[time_index].append(symbol) + bar_dict[time_index].note = symbol + for event in events: time_in_bar = event.time % Fraction(1) time_index = time_in_bar.numerator * ( bar_length / time_in_bar.denominator ) - bar_dict[time_index].append(event) + if isinstance(event, StopEvent): + bar_dict[time_index].stops.append(event) + elif isinstance(event, BPMEvent): + bar_dict[time_index].bpms.append(event) + bar = [] for i in range(bar_length): - events_and_note = bar_dict.get(i, []) - stops = list( - filter(lambda e: isinstance(e, StopEvent), events_and_note) - ) - bpms = list(filter(lambda e: isinstance(e, BPMEvent), events_and_note)) - notes = list(filter(lambda e: isinstance(e, str), events_and_note)) - assert len(notes) <= 1 - for stop in stops: + bar_event = bar_dict.get(i, BarEvent()) + for stop in bar_event.stops: bar.append(f"[{int(stop.duration * 1000)}]") - for bpm in bpms: + for bpm in bar_event.bpms: bar.append(f"({bpm.BPM})") - if notes: - note = notes[0] - else: - note = EMPTY_BEAT_SYMBOL - - bar.append(note) + bar.append(bar_event.note or EMPTY_BEAT_SYMBOL) bars[bar_index] = bar @@ -331,11 +331,13 @@ def _dump_memo2_chart( def _dump_memo2_internal(song: Song, circle_free: bool = False) -> List[ChartFile]: files: List[ChartFile] = [] for difficulty, chart in song.charts.items(): + timing = chart.timing or song.global_timing + assert timing is not None contents = _dump_memo2_chart( difficulty, chart, song.metadata, - chart.timing or song.global_timing, + timing, circle_free, ) files.append(ChartFile(contents, song, difficulty, chart)) diff --git a/jubeatools/formats/jubeat_analyser/memo2/load.py b/jubeatools/formats/jubeat_analyser/memo2/load.py index b1beef0..d5f4237 100644 --- a/jubeatools/formats/jubeat_analyser/memo2/load.py +++ b/jubeatools/formats/jubeat_analyser/memo2/load.py @@ -10,7 +10,7 @@ from typing import Dict, Iterator, List, Mapping, Optional, Set, Tuple, Union import constraint from more_itertools import collapse, mark_ends from parsimonious import Grammar, NodeVisitor, ParseError -from path import Path +from pathlib import Path from jubeatools.song import ( BeatsTime, @@ -23,6 +23,7 @@ from jubeatools.song import ( Song, TapNote, Timing, + Preview, ) from ..command import is_command, parse_command @@ -50,7 +51,7 @@ from ..symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS @dataclass -class Notes: +class NoteCluster: string: str def dump(self) -> str: @@ -73,7 +74,7 @@ class BPM: return f"({self.value})" -Event = Union[Notes, Stop, BPM] +Event = Union[NoteCluster, Stop, BPM] @dataclass @@ -91,7 +92,6 @@ class RawMemo2ChartLine: @dataclass class Memo2ChartLine: """timing part only contains notes""" - position: str timing: Optional[List[str]] @@ -136,7 +136,7 @@ class Memo2ChartLineVisitor(NodeVisitor): self.time_part.append(BPM(Decimal(value.text))) def visit_notes(self, node, visited_children): - self.time_part.append(Notes(node.text)) + self.time_part.append(NoteCluster(node.text)) def generic_visit(self, node, visited_children): ... @@ -229,11 +229,12 @@ class Memo2Parser(JubeatAnalyserParser): raise SyntaxError( f"Invalid chart line for #bpp={self.bytes_per_panel} : {raw_line}" ) + if raw_line.timing is not None and self.bytes_per_panel == 2: if any( - len(event.string.encode("shift-jis-2004")) % 2 != 0 - for event in raw_line.timing - if isinstance(event, Notes) + len(e.string.encode("shift-jis-2004")) % 2 != 0 + for e in raw_line.timing + if isinstance(e, NoteCluster) ): raise SyntaxError(f"Invalid chart line for #bpp=2 : {raw_line}") @@ -241,12 +242,12 @@ class Memo2Parser(JubeatAnalyserParser): line = Memo2ChartLine(raw_line.position, None) else: # split notes - bar = [] - for event in raw_line.timing: - if isinstance(event, Notes): - bar.extend(self._split_chart_line(event.string)) + bar: List[Union[str, Stop, BPM]] = [] + for raw_event in raw_line.timing: + if isinstance(raw_event, NoteCluster): + bar.extend(self._split_chart_line(raw_event.string)) else: - bar.append(event) + bar.append(raw_event) # extract timing info bar_length = sum(1 for e in bar if isinstance(e, str)) symbol_duration = BeatsTime(1, bar_length) @@ -286,8 +287,8 @@ class Memo2Parser(JubeatAnalyserParser): else: return list(line) - def _frames_duration(self) -> Decimal: - return sum(frame.duration for frame in self.frames) + def _frames_duration(self) -> BeatsTime: + return sum((frame.duration for frame in self.frames), start=BeatsTime(0)) def _push_frame(self): position_part = [ @@ -336,13 +337,13 @@ class Memo2Parser(JubeatAnalyserParser): def _iter_frames( self, - ) -> Iterator[Tuple[Mapping[str, BeatsTime], Memo2Frame, BeatsTime]]: + ) -> Iterator[Tuple[Mapping[str, BeatsTime], Memo2Frame]]: """iterate over tuples of (currently_defined_symbols, frame)""" - local_symbols: Dict[str, Decimal] = {} + local_symbols = {} frame_starting_beat = BeatsTime(0) for i, frame in enumerate(self.frames): if frame.timing_part: - frame_starting_beat = sum(f.duration for f in self.frames[:i]) + frame_starting_beat = sum((f.duration for f in self.frames[:i]), start=BeatsTime(0)) local_symbols = { symbol: frame_starting_beat + bar_index @@ -445,8 +446,7 @@ def _load_memo2_file(lines: List[str]) -> Song: cover=parser.jacket, ) if parser.preview_start is not None: - metadata.preview_start = SecondsTime(parser.preview_start) / 1000 - metadata.preview_length = SecondsTime(10) + metadata.preview = Preview(start=SecondsTime(parser.preview_start) / 1000,length=SecondsTime(10)) timing = Timing( events=parser.timing_events, beat_zero_offset=SecondsTime(parser.offset or 0) / 1000 diff --git a/jubeatools/formats/jubeat_analyser/mono_column/dump.py b/jubeatools/formats/jubeat_analyser/mono_column/dump.py index 5c7a67a..b2a6eb7 100644 --- a/jubeatools/formats/jubeat_analyser/mono_column/dump.py +++ b/jubeatools/formats/jubeat_analyser/mono_column/dump.py @@ -9,7 +9,7 @@ from itertools import chain from typing import Dict, Iterator, List, Mapping, Optional, Tuple from more_itertools import collapse, intersperse, mark_ends, windowed -from path import Path +from pathlib import Path from sortedcontainers import SortedKeyList from jubeatools import __version__ @@ -90,7 +90,7 @@ class MonoColumnDumpedSection(JubeatAnalyserDumpedSection): frame = {} time_in_section = note.time - self.current_beat if circle_free: - symbol = CIRCLE_FREE_SYMBOLS[time_in_section] + symbol = CIRCLE_FREE_SYMBOLS[int(time_in_section)] else: symbol = self.symbols[time_in_section] frame[note.position] = symbol @@ -177,11 +177,13 @@ def _dump_mono_column_chart( def _dump_mono_column_internal(song: Song, circle_free: bool) -> List[ChartFile]: files: List[ChartFile] = [] for difficulty, chart in song.charts.items(): + timing = chart.timing or song.global_timing + assert timing is not None contents = _dump_mono_column_chart( difficulty, chart, song.metadata, - chart.timing or song.global_timing, + timing, circle_free, ) files.append(ChartFile(contents, song, difficulty, chart)) diff --git a/jubeatools/formats/jubeat_analyser/mono_column/load.py b/jubeatools/formats/jubeat_analyser/mono_column/load.py index d0cb2a8..b841645 100644 --- a/jubeatools/formats/jubeat_analyser/mono_column/load.py +++ b/jubeatools/formats/jubeat_analyser/mono_column/load.py @@ -7,11 +7,11 @@ from decimal import Decimal from enum import Enum from functools import reduce from itertools import product -from typing import Dict, Iterator, List, Set, Tuple +from typing import Dict, Iterator, List, Set, Tuple, Union import constraint from parsimonious import Grammar, NodeVisitor, ParseError -from path import Path +from pathlib import Path from jubeatools.song import ( BeatsTime, @@ -24,7 +24,7 @@ from jubeatools.song import ( Song, TapNote, Timing, - Union, + Preview, ) from ..command import is_command, parse_command @@ -296,8 +296,10 @@ def _load_mono_column_file(lines: List[str]) -> Song: cover=parser.jacket, ) if parser.preview_start is not None: - metadata.preview_start = SecondsTime(parser.preview_start) / 1000 - metadata.preview_length = SecondsTime(10) + metadata.preview = Preview( + start=SecondsTime(parser.preview_start) / 1000, + length=SecondsTime(10) + ) timing = Timing( events=parser.timing_events, beat_zero_offset=SecondsTime(parser.offset) / 1000 diff --git a/jubeatools/formats/memon/__init__.py b/jubeatools/formats/memon/__init__.py index 625d84c..2b8b2ea 100644 --- a/jubeatools/formats/memon/__init__.py +++ b/jubeatools/formats/memon/__init__.py @@ -10,7 +10,7 @@ https://github.com/Stepland/memon from io import StringIO from itertools import chain -from typing import IO, Any, Dict, Iterable, List, Tuple, Union +from typing import IO, Any, Dict, Iterable, List, Tuple, Union, Mapping import simplejson as json from marshmallow import ( @@ -22,7 +22,7 @@ from marshmallow import ( validate, validates_schema, ) -from path import Path +from pathlib import Path from jubeatools.song import * from jubeatools.utils import lcm @@ -160,7 +160,7 @@ def load_memon_legacy(file: Path) -> Song: events=[BPMEvent(time=BeatsTime(0), BPM=memon["metadata"]["BPM"])], beat_zero_offset=SecondsTime(-memon["metadata"]["offset"]), ) - charts = MultiDict() + charts: MultiDict[Chart] = MultiDict() for memon_chart in memon["data"]: charts.add( memon_chart["dif_name"], @@ -187,7 +187,7 @@ def load_memon_0_1_0(file: Path) -> Song: events=[BPMEvent(time=BeatsTime(0), BPM=memon["metadata"]["BPM"])], beat_zero_offset=SecondsTime(-memon["metadata"]["offset"]), ) - charts = MultiDict() + charts: MultiDict[Chart] = MultiDict() for difficulty, memon_chart in memon["data"].items(): charts.add( difficulty, @@ -221,7 +221,7 @@ def load_memon_0_2_0(file: Path) -> Song: events=[BPMEvent(time=BeatsTime(0), BPM=memon["metadata"]["BPM"])], beat_zero_offset=SecondsTime(-memon["metadata"]["offset"]), ) - charts = MultiDict() + charts: MultiDict[Chart] = MultiDict() for difficulty, memon_chart in memon["data"].items(): charts.add( difficulty, @@ -254,7 +254,7 @@ def _get_timing(song: Song) -> Timing: return next( chart.timing for chart in song.charts.values() - if chart is not None + if chart.timing is not None ) def _raise_if_unfit_for_v0(song: Song, version: str) -> None: @@ -337,7 +337,7 @@ def dump_memon_legacy(song: Song, path: Path) -> Dict[Path, bytes]: _raise_if_unfit_for_v0(song, "legacy") timing = _get_timing(song) - memon = { + memon: Dict[str, Any] = { "metadata": { "song title": song.metadata.title, "artist": song.metadata.artist, @@ -364,7 +364,7 @@ def dump_memon_legacy(song: Song, path: Path) -> Dict[Path, bytes]: } ) - if path.isdir(): + if path.is_dir(): filepath = path / f"{song.metadata.title}.memon" else: filepath = path @@ -377,7 +377,7 @@ def dump_memon_0_1_0(song: Song, path: Path) -> Dict[Path, bytes]: _raise_if_unfit_for_v0(song, "v0.1.0") timing= _get_timing(song) - memon = { + memon: Dict[str, Any] = { "version": "0.1.0", "metadata": { "song title": song.metadata.title, @@ -400,7 +400,7 @@ def dump_memon_0_1_0(song: Song, path: Path) -> Dict[Path, bytes]: ], } - if path.isdir(): + if path.is_dir(): filepath = path / f"{song.metadata.title}.memon" else: filepath = path @@ -413,7 +413,7 @@ def dump_memon_0_2_0(song: Song, path: Path) -> Dict[Path, bytes]: _raise_if_unfit_for_v0(song, "v0.2.0") timing = _get_timing(song) - memon = { + memon: Dict[str, Any] = { "version": "0.2.0", "metadata": { "song title": song.metadata.title, @@ -443,7 +443,7 @@ def dump_memon_0_2_0(song: Song, path: Path) -> Dict[Path, bytes]: ], } - if path.isdir(): + if path.is_dir(): filepath = path / f"{song.metadata.title}.memon" else: filepath = path diff --git a/jubeatools/formats/memon/test_memon.py b/jubeatools/formats/memon/test_memon.py index 0438fb2..9a1a25a 100644 --- a/jubeatools/formats/memon/test_memon.py +++ b/jubeatools/formats/memon/test_memon.py @@ -2,7 +2,7 @@ import tempfile import hypothesis.strategies as st from hypothesis import given -from path import Path +from pathlib import Path from jubeatools.testutils.strategies import NoteOption, TimingOption from jubeatools.testutils.strategies import song as song_strat diff --git a/jubeatools/formats/typing.py b/jubeatools/formats/typing.py index cd307f9..e1db37e 100644 --- a/jubeatools/formats/typing.py +++ b/jubeatools/formats/typing.py @@ -1,12 +1,8 @@ -from typing import Any, Dict, Protocol +from typing import Any, Dict, Callable -from path import Path +from pathlib import Path from jubeatools.song import Song -class Dumper(Protocol): - def __call__( - self, song: Song, path: Path, **kwargs: Dict[str, Any] - ) -> Dict[Path, bytes]: - ... +Dumper = Callable[[Song, Path], Dict[Path, bytes]] \ No newline at end of file diff --git a/jubeatools/song.py b/jubeatools/song.py index 95632b0..32850de 100644 --- a/jubeatools/song.py +++ b/jubeatools/song.py @@ -15,7 +15,7 @@ from functools import wraps from typing import Iterator, List, Mapping, Optional, Type, Union from multidict import MultiDict -from path import Path +from pathlib import Path BeatsTime = Fraction SecondsTime = Decimal @@ -164,7 +164,7 @@ class Song: f"{self.metadata}\n" f"{other.metadata}" ) - charts = MultiDict() + charts: MultiDict[Chart] = MultiDict() charts.extend(self.charts) charts.extend(other.charts) if ( diff --git a/jubeatools/testutils/strategies.py b/jubeatools/testutils/strategies.py index 3d98caf..43767da 100644 --- a/jubeatools/testutils/strategies.py +++ b/jubeatools/testutils/strategies.py @@ -231,7 +231,7 @@ def song( ), ) diffs = draw(st.sets(diff_name_strat, min_size=1, max_size=10)) - charts = MultiDict() + charts: MultiDict[Chart] = MultiDict() for diff_name in diffs: chart_timing_strat = st.none() if TimingOption.PER_CHART in timing_options: