diff --git a/src/install/filesize.py b/src/install/filesize.py index de92f8c..b18ef68 100644 --- a/src/install/filesize.py +++ b/src/install/filesize.py @@ -8,7 +8,7 @@ # Copyright (C) Martijn Faassen, Startifact # -from ._types import SizeSystem +from ..typings import SizeSystem traditional: SizeSystem = [ (1024 ** 5, 'P'), diff --git a/src/install/loadingbar.py b/src/install/loadingbar.py index e39b479..1097eea 100644 --- a/src/install/loadingbar.py +++ b/src/install/loadingbar.py @@ -15,20 +15,66 @@ # along with this program. If not, see . from os import get_terminal_size -from typing import Collection, Generic, Literal, TypeVar, Optional, Type -from types import TracebackType +from typing import ( + Collection, Generic, Literal, TypeVar, + Optional, overload, TYPE_CHECKING +) + +if TYPE_CHECKING: + from types import TracebackType from .filesize import size, traditional -T = TypeVar("T") +_T = TypeVar("_T") + + +class loadingbar(Generic[_T]): + @overload + def __init__( + # total + self, iterator: Collection[_T], /, *, + unit: Literal['it', 'B'] = 'it', + bar_length: Optional[int] = None, + bar_format: str = "{title} {percentage:3.0f}% [{bar}] {current}/{total}", + title: str = '', + show_desc: bool = False, + desc: str = '', + disappear: bool = False, + ) -> None: ... + @overload + def __init__( + # total + self, /, *, + total: _T, + unit: Literal['it', 'B'] = 'it', + bar_length: Optional[int] = None, + bar_format: str = "{title} {percentage:3.0f}% [{bar}] {current}/{total}", + title: str = '', + show_desc: bool = False, + desc: str = '', + disappear: bool = False, + ) -> None: ... -class loadingbar(Generic[T]): + @overload def __init__( - self, iterator: Collection[T] | None = None, /, *, + # both + self, iterator: Optional[Collection[_T]] = None, /, *, total: int = 0, unit: Literal['it', 'B'] = 'it', - bar_length: int | None = None, + bar_length: Optional[int] = None, + bar_format: str = "{title} {percentage:3.0f}% [{bar}] {current}/{total}", + title: str = '', + show_desc: bool = False, + desc: str = '', + disappear: bool = False, + ) -> None: ... + + def __init__( + self, iterator: Optional[Collection[_T]] = None, /, *, + total: int | _T = 0, + unit: Literal['it', 'B'] = 'it', + bar_length: Optional[int] = None, bar_format: str = "{title} {percentage:3.0f}% [{bar}] {current}/{total}", title: str = '', show_desc: bool = False, @@ -54,21 +100,18 @@ def __init__( :param disappear: If you want the bar to disappear """ - # Define class variables - self._iter_type: Literal['iter', 'total', 'both'] - if iterator is not None and total == 0: - self._iter_type = 'iter' + # iter self.iterator = iterator self.iterable = iter(iterator) self.iterator_len = len(iterator) elif iterator is None and total != 0: - self._iter_type = 'total' + # total self.iterator = None self.iterable = None self.total = total elif iterator is not None and total != 0: - self._iter_type = 'both' + # both self.iterator = iterator self.iterable = iter(iterator) self.iterator_len = len(iterator) @@ -112,40 +155,44 @@ def __init__( else: self.bar_length = bar_length - def __iter__(self) -> "loadingbar[T]": + def __iter__(self) -> "loadingbar[_T]": "Method that allows iterators" return self - def __next__(self) -> T: + def __next__(self) -> _T: "Go to the next iteration" # Update the item and idx if self.iterable is not None: self.item = next(self.iterable) self.idx += 1 - elif self.idx < self.total: # 'total' - self.idx += 1 else: - raise StopIteration + if TYPE_CHECKING and not isinstance(self.total, int): + raise TypeError + + if self.idx < self.total: # total + self.idx += 1 + else: + raise StopIteration # Refresh the loading bar self.refresh() # Return the item - if self.iterator is not None: # 'both' or 'iter' + if self.iterator is not None: # both or iter return self.item else: return self.idx # type: ignore - def __enter__(self) -> "loadingbar[T]": + def __enter__(self) -> "loadingbar[_T]": "Allow a with-as statement" return self - def __exit__(self, exc_type: Optional[Type[BaseException]], + def __exit__(self, exc_type: Optional[type[BaseException]], exc_value: Optional[BaseException], - traceback: Optional[TracebackType]) -> bool: + traceback: Optional['TracebackType']) -> bool: "Allow a with-as statement" return False # No errors @@ -154,7 +201,10 @@ def refresh(self) -> None: "Refresh the loading bar, called automatically" # Calculate progress - if self._iter_type in ('both', 'total'): + if hasattr(self, 'total'): # both, total + if TYPE_CHECKING and not isinstance(self.total, int): + raise TypeError + percent = round(self.idx / self.total * 100, 0) else: percent = round(self.idx / self.iterator_len * 100, 0) @@ -165,12 +215,16 @@ def refresh(self) -> None: # Define the current and total if self.unit == 'it': current = str(self.idx) - total = str(self.total if self._iter_type in ( - 'both', 'total') else self.iterator_len) + total = str(self.total if hasattr( + self, 'total') else self.iterator_len) else: current = size(self.idx, traditional) - total = size(self.total if self._iter_type in ( - 'both', 'total') else self.iterator_len, traditional) + arg = self.total if hasattr(self, 'total') else self.iterator_len + + if TYPE_CHECKING and not isinstance(arg, int): + raise TypeError + + total = size(arg, traditional) # Define the text length text_length = self.formatting_length + \ @@ -181,12 +235,10 @@ def refresh(self) -> None: # Print the loading bar start = '\033[F\r' if self.show_desc else '\r' - if self.show_desc and self._new_desc: - end = '\n\033[K' + self.desc - elif self.show_desc: - end = '\n' + self.desc - else: - end = '' + end = ( + '\n\033[K' + self.desc if self.show_desc and self._new_desc + else '\n' + self.desc if self.show_desc else '' + ) print(start + self.bar_format.format( title=self.title, @@ -197,8 +249,7 @@ def refresh(self) -> None: ), end=end) # Clear the loading bar at the end - if self.idx == (self.total if self._iter_type in ( - 'both', 'total') else self.iterator_len): + if self.idx == (self.total if hasattr(self, 'total') else self.iterator_len): if self.disappear and self.show_desc: print('\r\033[K\033[F\r\033[K', end='') elif self.disappear: @@ -208,7 +259,7 @@ def refresh(self) -> None: def update(self, amount: int) -> None: "Add 'n' amount of iterations to the loading bar" - if self._iter_type == 'iter': + if not hasattr(self, 'total'): i = 0 # Call next(self) while less than amount diff --git a/src/install/media.py b/src/install/media.py index abf6a23..c74e71a 100644 --- a/src/install/media.py +++ b/src/install/media.py @@ -24,7 +24,7 @@ from .modloaders import inst_modloader, MINECRAFT_DIR from .filesize import size, alternative -from ._types import Manifest, URLMedia, Media, MediaList, Side +from ..typings import Manifest, URLMedia, Media, MediaList, Side class prepare: @@ -218,7 +218,6 @@ def download_files(total_size: int, install_path: str, side: Side, manifest: Man show_desc=True, disappear=True ) as bar: - bar: loadingbar[int] # The only way it worked out for url, fname, fsize, sides in iterator: if side not in sides: # As the size isn't calculated, it @@ -307,7 +306,8 @@ def install( # Print the mod info print( - f"\n{len(mods)} mods, {len(resourcepacks)} recourcepacks, {len(shaderpacks)} shaderpacks\n" + f"\n{len(mods)} mods, {len(resourcepacks)} " + f"recourcepacks, {len(shaderpacks)} shaderpacks\n" f"Total file size: {size(total_size, system=alternative)}" ) diff --git a/src/install/modloaders.py b/src/install/modloaders.py index e081e31..f10fc16 100644 --- a/src/install/modloaders.py +++ b/src/install/modloaders.py @@ -15,6 +15,7 @@ # along with this program. If not, see . from atexit import register +from http.client import HTTPResponse from json import load, loads, dump from os import path, getenv, mkdir, rename from shutil import rmtree, copyfile @@ -24,16 +25,16 @@ from urllib import request from zipfile import ZipFile -from .urls import forge as forge_urls, fabric as fabric_urls from .loadingbar import loadingbar +from .urls import forge as forge_urls from ..common.maven_coords import maven_parse -from ._types import ( +from ..typings import ( Client, Server, Side, Modloader, - MinecraftJson, VersionJson, + ForgeLibrary, ForgeVersionJson, InstallProfile, Libraries, - Library, OSLibrary + MinecraftJson, OSLibrary ) # Define the minecraft directory @@ -105,13 +106,14 @@ def __init__(self, mc_version: str, "Please make sure you have Java installed and it is properly configured." ) - self.minecraft_json: MinecraftJson = loads(request.urlopen( - [item for item in loads( - request.urlopen( - forge_urls.version_manifest_v2 - ).read().decode('utf-8') - )['versions'] if item['id'] == mc_version][0]['url'] - ).read().decode('utf-8')) + v_man_v2: HTTPResponse = request.urlopen( + forge_urls.version_manifest_v2) + + for item in loads(v_man_v2.read().decode('utf-8'))['versions']: + if item['id'] == mc_version: + _res: HTTPResponse = request.urlopen(item['url']) + self.minecraft_json: MinecraftJson = loads( + _res.read().decode('utf-8')) # Exit if the launcher hasn't launched once if not path.isfile(path.join(launcher_dir, 'launcher_profiles.json')) and side == 'client': @@ -127,7 +129,7 @@ def __init__(self, mc_version: str, with archive.open('install_profile.json') as fp: self.install_profile: InstallProfile = load(fp) with archive.open('version.json') as fp: - self.version_json: VersionJson = load(fp) + self.version_json: ForgeVersionJson = load(fp) # Define the forge dir forge_dir = path.join( @@ -203,8 +205,10 @@ def download_jar_files(self) -> None: if self.side == 'client': # Make the required directories - for directory in [path.join(self.launcher_dir, 'versions'), - path.join(self.launcher_dir, 'versions', self.mc_version)]: + for directory in [ + path.join(self.launcher_dir, 'versions'), + path.join(self.launcher_dir, 'versions', self.mc_version) + ]: if not path.isdir(directory): mkdir(directory) @@ -227,7 +231,7 @@ def download_jar_files(self) -> None: break mod_file.write(resp_data) - def download_library(self, bar: loadingbar[Library | OSLibrary], library: Library) -> None: + def download_library(self, bar: loadingbar[ForgeLibrary | OSLibrary], library: ForgeLibrary) -> None: """Download a library""" # Define the java os names osdict = { @@ -384,33 +388,7 @@ def build_processors(self) -> None: class fabric: - "I accidentally uploaded this, it's unfinished" - - def __init__(self, mc_version: str, - fabric_version: str, - side: Side, - install_dir: str = MINECRAFT_DIR, - launcher_dir: str = MINECRAFT_DIR) -> None: - - self.mc_version = mc_version - - # Define the class variables - launcher_dir = install_dir if side == 'server' else launcher_dir - - self.mc_version = mc_version - self.fabric_version = fabric_version - self.side = side - self.install_dir = install_dir - self.launcher_dir = launcher_dir - - self.install_version() - - def install_version(self): - print(request.urlopen(fabric_urls.api_url( - 'v2', 'versions', 'loader', - self.mc_version, self.fabric_version, - 'server', 'json' - )).read().decode('utf-8')) + pass def inst_modloader( diff --git a/src/install/urls.py b/src/install/urls.py index b669b35..b4d5828 100644 --- a/src/install/urls.py +++ b/src/install/urls.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from ._types import Media +from ..typings import Media def media_url(media: Media) -> str: @@ -44,9 +44,3 @@ class forge: def forge_installer_url(mc_version: str, forge_version: str) -> str: return "https://maven.minecraftforge.net/net/minecraftforge/forge/" \ f"{mc_version}-{forge_version}/forge-{mc_version}-{forge_version}-installer.jar" - - -class fabric: - @staticmethod - def api_url(*paths: str) -> str: - return "https://meta.fabricmc.net/" + '/'.join(paths) diff --git a/src/runner.py b/src/runner.py index 324e50e..54408cc 100644 --- a/src/runner.py +++ b/src/runner.py @@ -21,7 +21,7 @@ from .install.media import install from .install.modloaders import MINECRAFT_DIR -from .install._types import Side +from .typings import Side class Options(TypedDict): diff --git a/src/install/_types.py b/src/typings.py similarity index 67% rename from src/install/_types.py rename to src/typings.py index 155883f..907c2eb 100644 --- a/src/install/_types.py +++ b/src/typings.py @@ -19,11 +19,15 @@ Generic, NotRequired, Optional ) -# --- filesize.py --- # +# ============================ # +# install/filesize.py # +# ============================ # SizeSystem = list[tuple[int, str | tuple[str, str]]] -# --- common --- # +# ============================ # +# install/ # +# ============================ # Client = Literal['client'] Server = Literal['server'] Side = Literal['client', 'server'] @@ -31,12 +35,12 @@ _T = TypeVar("_T", bound=Literal['cf', 'pm', 'mr', 'url']) -class Minecraft(TypedDict): +class _Minecraft(TypedDict): version: str modloader: str -class Info(TypedDict, total=False): +class _Info(TypedDict, total=False): "Dictionary with info about media" title: str icon: str @@ -49,7 +53,7 @@ class _Media(Generic[_T], TypedDict): slug: str name: str sides: list[Literal['client', 'server']] - info: NotRequired[Info] + info: NotRequired[_Info] # Download info: url, path, size _dl: NotRequired[tuple[str, str, int]] @@ -82,23 +86,30 @@ class URLMedia(_Media[Literal['url']]): class Manifest(TypedDict): "All information of the modpack" - minecraft: Minecraft + minecraft: _Minecraft mods: MediaList resourcepacks: MediaList shaderpacks: MediaList -# --- modloaders.py --- # +# ============================ # +# install/modloaders.py # +# ============================ # Modloader = Literal['forge', 'fabric'] | str +# ========= # +# Forge # +# ========= # + +# Libraries class _OSDict(TypedDict): name: Literal["windows", "linux", "osx"] # NotRequired arch: NotRequired[Literal["x86"]] -class Library(TypedDict): - "All libraries of the media" +class ForgeLibrary(TypedDict): + "A forge library" path: str sha1: str size: int @@ -111,15 +122,15 @@ class _Rules(TypedDict): os: _OSDict # NotRequired -class OSLibrary(Library, _Rules): - "All libraries of the media for a specific os" - +class OSLibrary(ForgeLibrary, _Rules): + "A library for a specific OS" ... -Libraries = dict[str, Library | OSLibrary] +Libraries = dict[str, ForgeLibrary | OSLibrary] +# Forge minecraft and version json class _Arg(TypedDict): rules: list[_Rules] value: str | list[str] @@ -167,12 +178,6 @@ class _Download(TypedDict): artifact: _Artifact -class _JavaLibrary(TypedDict): - downloads: _Download - name: str - rules: NotRequired[list[_Rules]] - - class _File(TypedDict): id: str sha1: str @@ -190,10 +195,13 @@ class _Logging(TypedDict): client: _LoggingClient -class JavaJson(TypedDict): +_L = TypeVar('_L') + + +class _JavaJson(TypedDict, Generic[_L]): arguments: _Arguments id: str - libraries: list[_JavaLibrary] + libraries: list[_L] logging: _Logging mainClass: str releaseTime: str @@ -201,7 +209,13 @@ class JavaJson(TypedDict): type: str -class MinecraftJson(JavaJson): +class _ForgeLibrary(TypedDict): + downloads: _Download + name: str + rules: NotRequired[list[_Rules]] + + +class MinecraftJson(_JavaJson[_ForgeLibrary]): "Minecraft version_manifest.json file" assetIndex: _AssetIndex assets: str @@ -211,12 +225,12 @@ class MinecraftJson(JavaJson): minimumLauncherVersion: int -class VersionJson(JavaJson): - "Minecraft version.json file" +class ForgeVersionJson(_JavaJson[_ForgeLibrary]): + "Forge's minecraft version.json file" inheritsFrom: str -# -- Install profile -- # +# Install profile class _Data(TypedDict): client: str server: str @@ -238,9 +252,71 @@ class InstallProfile(TypedDict): serverJarPath: str data: dict[str, _Data] processors: list[_Processor] - libraries: list[_JavaLibrary] + libraries: list[_ForgeLibrary] icon: str json: str logo: str mirrorList: str welcome: str + + +# ========== # +# Fabric # +# ========== # + +# Loader +class _Loader(TypedDict): + seperator: str + build: int + maven: str + version: str + stable: bool + + +class _Intermediary(TypedDict): + maven: str + version: str + stable: bool + + +class _FabricLibrary(TypedDict): + name: str + url: str + + +class _FabricLibraries(TypedDict): + client: list[_FabricLibrary] + common: list[_FabricLibrary] + server: list[_FabricLibrary] + + +class _MainClass(TypedDict): + client: str + server: str + + +class _LauncherMeta(TypedDict): + version: int + libraries: _FabricLibraries + mainClass: _MainClass + + +class LoaderJson(TypedDict): + "Information about the fabric loader" + loader: _Loader + intermediary: _Intermediary + launcherMeta: _LauncherMeta + + +class FabricLibrary(_FabricLibrary): + "A fabric library" + file: str + + +LibraryList = list[FabricLibrary] + + +# Fabric version json +class FabricVersionJson(_JavaJson[_FabricLibrary]): + "Fabric's minecraft version.json file" + inheritsFrom: str diff --git a/tests/install/test_loadingbar.py b/tests/install/test_loadingbar.py index de38111..365c8d3 100644 --- a/tests/install/test_loadingbar.py +++ b/tests/install/test_loadingbar.py @@ -53,7 +53,7 @@ def test_bar(self): self.assertEqual(bar.desc, "Test description 2") # Updating with .update() and using total=int - bar_2: loadingbar[int] = loadingbar(total=100) + bar_2 = loadingbar(total=100) bar_2.update(50) self.assertEqual(bar_2.idx, 50) diff --git a/tests/install/test_urls.py b/tests/install/test_urls.py index 79ae68f..3cc8af9 100644 --- a/tests/install/test_urls.py +++ b/tests/install/test_urls.py @@ -17,7 +17,7 @@ import unittest from src.install.urls import media_url, forge -from src.install._types import Media +from src.typings import Media class Urls(unittest.TestCase):