From e36c6a31fe5387ed18f3eb55f9918edaa8bb1f98 Mon Sep 17 00:00:00 2001 From: Dunedan Date: Sun, 25 Aug 2024 06:29:39 +0200 Subject: [PATCH] Enable additional ruff rules In the ruff config file added in #6954 explicitly selecting the ruff rules to check was missed, resulting in ruff only checking a very small subset of its available rules. That hasn't been desired, so this is the first of a series of commits enabling more rules. In this PR all rules whose violations can be either automatically fixed by ruff or are trivial to fix manually get enabled. For the follow up PRs it's intended to focus on one area of rules per PR to gradually improve the Python code quality. --- ruff.toml | 53 +++++++ source/collada/tests/tests.py | 30 ++-- source/tools/entity/checkrefs.py | 78 +++++----- source/tools/entity/creationgraph.py | 11 +- source/tools/entity/entvalidate.py | 28 ++-- source/tools/entity/scriptlib/__init__.py | 27 ++-- source/tools/fontbuilder2/FontLoader.py | 8 +- source/tools/fontbuilder2/Packer.py | 37 +++-- source/tools/fontbuilder2/__init__.py | 0 source/tools/fontbuilder2/dumpfontchars.py | 2 +- source/tools/fontbuilder2/fontbuilder.py | 20 +-- source/tools/i18n/checkDiff.py | 19 ++- source/tools/i18n/checkTranslations.py | 23 ++- source/tools/i18n/cleanTranslationFiles.py | 10 +- source/tools/i18n/creditTranslators.py | 12 +- source/tools/i18n/extractors/extractors.py | 88 +++++------ source/tools/i18n/generateDebugTranslation.py | 9 +- source/tools/i18n/i18n_helper/__init__.py | 1 + source/tools/i18n/i18n_helper/catalog.py | 2 +- source/tools/i18n/i18n_helper/globber.py | 4 +- source/tools/i18n/pullTranslations.py | 4 +- source/tools/i18n/tests/__init__.py | 0 source/tools/i18n/tests/test_checkDiff.py | 2 + source/tools/i18n/updateTemplates.py | 31 ++-- source/tools/mapcompatibility/a18_to_a19.py | 79 +++++----- source/tools/rlclient/python/__init__.py | 0 .../tools/rlclient/python/samples/__init__.py | 0 .../rlclient/python/samples/simple-example.py | 12 +- source/tools/rlclient/python/setup.py | 1 + .../tools/rlclient/python/tests/__init__.py | 0 .../rlclient/python/tests/test_actions.py | 10 +- .../rlclient/python/tests/test_evaluate.py | 8 +- .../tools/rlclient/python/zero_ad/__init__.py | 7 +- source/tools/rlclient/python/zero_ad/api.py | 4 +- .../rlclient/python/zero_ad/environment.py | 19 ++- source/tools/spirv/compile.py | 141 +++++++++--------- source/tools/templatesanalyzer/unitTables.py | 50 +++---- source/tools/xmlvalidator/validate_grammar.py | 34 ++--- source/tools/xmlvalidator/validator.py | 80 +++++----- 39 files changed, 509 insertions(+), 435 deletions(-) mode change 100644 => 100755 source/collada/tests/tests.py mode change 100644 => 100755 source/tools/entity/checkrefs.py mode change 100644 => 100755 source/tools/entity/creationgraph.py mode change 100644 => 100755 source/tools/entity/entvalidate.py create mode 100644 source/tools/fontbuilder2/__init__.py mode change 100644 => 100755 source/tools/fontbuilder2/fontbuilder.py mode change 100644 => 100755 source/tools/i18n/checkDiff.py mode change 100644 => 100755 source/tools/i18n/checkTranslations.py mode change 100644 => 100755 source/tools/i18n/cleanTranslationFiles.py mode change 100644 => 100755 source/tools/i18n/extractors/extractors.py mode change 100644 => 100755 source/tools/i18n/generateDebugTranslation.py mode change 100644 => 100755 source/tools/i18n/pullTranslations.py create mode 100644 source/tools/i18n/tests/__init__.py mode change 100644 => 100755 source/tools/i18n/updateTemplates.py create mode 100644 source/tools/rlclient/python/__init__.py create mode 100644 source/tools/rlclient/python/samples/__init__.py create mode 100644 source/tools/rlclient/python/tests/__init__.py mode change 100644 => 100755 source/tools/spirv/compile.py mode change 100644 => 100755 source/tools/templatesanalyzer/unitTables.py mode change 100644 => 100755 source/tools/xmlvalidator/validate_grammar.py mode change 100644 => 100755 source/tools/xmlvalidator/validator.py diff --git a/ruff.toml b/ruff.toml index 4ca9e6080d..b1d6a61148 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,58 @@ line-length = 99 +[format] +line-ending = "lf" + +[lint] +select = ["ALL"] +ignore = [ + "A", + "ARG", + "ANN", + "B018", + "B023", + "C90", + "COM812", + "D", + "DTZ005", + "EM", + "ERA", + "FA", + "FIX", + "FBT", + "ISC001", + "N", + "PERF203", + "PERF401", + "PLR0912", + "PLR0913", + "PLR0915", + "PLR1704", + "PLR2004", + "PLW2901", + "PT", + "PTH", + "RUF012", + "S101", + "S310", + "S314", + "S324", + "S320", + "S603", + "S607", + "SIM102", + "SIM105", + "SIM113", + "SIM115", + "T20", + "TD", + "TRY002", + "TRY003", + "TRY004", + "UP038", + "W505" +] + [lint.isort] lines-after-imports = 2 diff --git a/source/collada/tests/tests.py b/source/collada/tests/tests.py old mode 100644 new mode 100755 index 49a49c5e7f..3bfeec9c79 --- a/source/collada/tests/tests.py +++ b/source/collada/tests/tests.py @@ -2,9 +2,10 @@ # ruff: noqa: F403, F405 -from ctypes import * import os import xml.etree.ElementTree as ET +from ctypes import * + binaries = "../../../binaries" @@ -16,20 +17,20 @@ dll_filename = { # The DLL may need other DLLs which are in its directory, so set the path to that # (Don't care about clobbering the old PATH - it doesn't have anything important) -os.environ["PATH"] = "%s/system/" % binaries +os.environ["PATH"] = f"{binaries}/system/" # Load the actual library -library = cdll.LoadLibrary("%s/system/%s" % (binaries, dll_filename)) +library = cdll.LoadLibrary(f"{binaries}/system/{dll_filename}") def log(severity, message): - print("[%s] %s" % (("INFO", "WARNING", "ERROR")[severity], message)) + print("[{}] {}".format(("INFO", "WARNING", "ERROR")[severity], message)) clog = CFUNCTYPE(None, c_int, c_char_p)(log) # (the CFUNCTYPE must not be GC'd, so try to keep a reference) library.set_logger(clog) -skeleton_definitions = open("%s/data/tests/collada/skeletons.xml" % binaries).read() +skeleton_definitions = open(f"{binaries}/data/tests/collada/skeletons.xml").read() library.set_skeleton_definitions(skeleton_definitions, len(skeleton_definitions)) @@ -115,15 +116,16 @@ clean_dir(test_mod + "/art/meshes") clean_dir(test_mod + "/art/actors") clean_dir(test_mod + "/art/animation") -# for test_file in ['cube', 'jav2', 'jav2b', 'teapot_basic', 'teapot_skin', 'plane_skin', 'dude_skin', 'mergenonbone', 'densemesh']: +# for test_file in ['cube', 'jav2', 'jav2b', 'teapot_basic', 'teapot_skin', 'plane_skin', +# 'dude_skin', 'mergenonbone', 'densemesh']: # for test_file in ['teapot_basic', 'jav2b', 'jav2d']: for test_file in ["xsitest3c", "xsitest3e", "jav2d", "jav2d2"]: # for test_file in ['xsitest3']: # for test_file in []: - print("* Converting PMD %s" % (test_file)) + print(f"* Converting PMD {test_file}") - input_filename = "%s/%s.dae" % (test_data, test_file) - output_filename = "%s/art/meshes/%s.pmd" % (test_mod, test_file) + input_filename = f"{test_data}/{test_file}.dae" + output_filename = f"{test_mod}/art/meshes/{test_file}.pmd" input = open(input_filename).read() output = convert_dae_to_pmd(input) @@ -140,18 +142,18 @@ for test_file in ["xsitest3c", "xsitest3e", "jav2d", "jav2d2"]: ], [("helmet", "teapot_basic_static")], ) - open("%s/art/actors/%s.xml" % (test_mod, test_file), "w").write(xml) + open(f"{test_mod}/art/actors/{test_file}.xml", "w").write(xml) xml = create_actor_static(test_file, "male") - open("%s/art/actors/%s_static.xml" % (test_mod, test_file), "w").write(xml) + open(f"{test_mod}/art/actors/{test_file}_static.xml", "w").write(xml) # for test_file in ['jav2','jav2b', 'jav2d']: for test_file in ["xsitest3c", "xsitest3e", "jav2d", "jav2d2"]: # for test_file in []: - print("* Converting PSA %s" % (test_file)) + print(f"* Converting PSA {test_file}") - input_filename = "%s/%s.dae" % (test_data, test_file) - output_filename = "%s/art/animation/%s.psa" % (test_mod, test_file) + input_filename = f"{test_data}/{test_file}.dae" + output_filename = f"{test_mod}/art/animation/{test_file}.psa" input = open(input_filename).read() output = convert_dae_to_psa(input) diff --git a/source/tools/entity/checkrefs.py b/source/tools/entity/checkrefs.py old mode 100644 new mode 100755 index 40ee93fbe8..37ac8000c2 --- a/source/tools/entity/checkrefs.py +++ b/source/tools/entity/checkrefs.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 +import sys from argparse import ArgumentParser from io import BytesIO from json import load, loads +from logging import INFO, WARNING, Filter, Formatter, StreamHandler, getLogger +from os.path import basename, exists, sep from pathlib import Path -from re import split, match -from struct import unpack, calcsize -from os.path import sep, exists, basename -from xml.etree import ElementTree -import sys +from re import match, split +from struct import calcsize, unpack +from xml.etree import ElementTree as ET + from scriptlib import SimulTemplateEntity, find_files -from logging import WARNING, getLogger, StreamHandler, INFO, Formatter, Filter class SingleLevelFilter(Filter): @@ -20,8 +21,7 @@ class SingleLevelFilter(Filter): def filter(self, record): if self.reject: return record.levelno != self.passlevel - else: - return record.levelno == self.passlevel + return record.levelno == self.passlevel class CheckRefs: @@ -80,14 +80,15 @@ class CheckRefs: "-a", "--validate-actors", action="store_true", - help="run the validator.py script to check if the actors files have extra or missing textures." - " This currently only works for the public mod.", + help="run the validator.py script to check if the actors files have extra or missing " + "textures. This currently only works for the public mod.", ) ap.add_argument( "-t", "--validate-templates", action="store_true", - help="run the validator.py script to check if the xml files match their (.rng) grammar file.", + help="run the validator.py script to check if the xml files match their (.rng) " + "grammar file.", ) ap.add_argument( "-m", @@ -105,8 +106,8 @@ class CheckRefs: self.mods = list( dict.fromkeys([*args.mods, *self.get_mod_dependencies(*args.mods), "mod"]).keys() ) - self.logger.info(f"Checking {'|'.join(args.mods)}'s integrity.") - self.logger.info(f"The following mods will be loaded: {'|'.join(self.mods)}.") + self.logger.info("Checking %s's integrity.", "|".join(args.mods)) + self.logger.info("The following mods will be loaded: %s.", "|".join(self.mods)) if args.check_map_xml: self.add_maps_xml() self.add_maps_pmp() @@ -185,7 +186,7 @@ class CheckRefs: for fp, ffp in sorted(mapfiles): self.files.append(str(fp)) self.roots.append(str(fp)) - et_map = ElementTree.parse(ffp).getroot() + et_map = ET.parse(ffp).getroot() entities = et_map.find("Entities") used = ( {entity.find("Template").text.strip() for entity in entities.findall("Entity")} @@ -211,7 +212,7 @@ class CheckRefs: def add_maps_pmp(self): self.logger.info("Loading maps PMP...") # Need to generate terrain texture filename=>relative path lookup first - terrains = dict() + terrains = {} for fp, ffp in self.find_files("art/terrains", "xml"): name = fp.stem # ignore terrains.xml @@ -219,7 +220,10 @@ class CheckRefs: if name in terrains: self.inError = True self.logger.error( - f"Duplicate terrain name '{name}' (from '{terrains[name]}' and '{ffp}')" + "Duplicate terrain name '%s' (from '%s' and '%s')", + name, + terrains[name], + ffp, ) terrains[name] = str(fp) mapfiles = self.find_files("maps/scenarios", "pmp") @@ -241,7 +245,7 @@ class CheckRefs: (mapsize,) = unpack(int_fmt, f.read(int_len)) f.seek(2 * (mapsize * 16 + 1) * (mapsize * 16 + 1), 1) # skip heightmap (numtexs,) = unpack(int_fmt, f.read(int_len)) - for i in range(numtexs): + for _i in range(numtexs): (length,) = unpack(int_fmt, f.read(int_len)) terrain_name = f.read(length).decode("ascii") # suppose ascii encoding self.deps.append( @@ -279,7 +283,8 @@ class CheckRefs: def add_entities(self): self.logger.info("Loading entities...") simul_templates_path = Path("simulation/templates") - # TODO: We might want to get computed templates through the RL interface instead of computing the values ourselves. + # TODO: We might want to get computed templates through the RL interface instead of + # computing the values ourselves. simul_template_entity = SimulTemplateEntity(self.vfs_root, self.logger) custom_phase_techs = self.get_custom_phase_techs() for fp, _ in sorted(self.find_files(simul_templates_path, "xml")): @@ -307,7 +312,8 @@ class CheckRefs: actor = entity.find("VisualActor").find("Actor") if "{phenotype}" in actor.text: for phenotype in phenotypes: - # See simulation2/components/CCmpVisualActor.cpp and Identity.js for explanation. + # See simulation2/components/CCmpVisualActor.cpp and Identity.js + # for explanation. actor_path = actor.text.replace("{phenotype}", phenotype) self.deps.append((str(fp), f"art/actors/{actor_path}")) else: @@ -332,7 +338,8 @@ class CheckRefs: if sound_group.text and sound_group.text.strip(): if "{phenotype}" in sound_group.text: for phenotype in phenotypes: - # see simulation/components/Sound.js and Identity.js for explanation + # see simulation/components/Sound.js and Identity.js + # for explanation sound_path = sound_group.text.replace( "{phenotype}", phenotype ).replace("{lang}", lang) @@ -483,7 +490,7 @@ class CheckRefs: for fp, ffp in sorted(self.find_files("art/actors", "xml")): self.files.append(str(fp)) self.roots.append(str(fp)) - root = ElementTree.parse(ffp).getroot() + root = ET.parse(ffp).getroot() if root.tag == "actor": self.append_actor_dependencies(root, fp) @@ -500,7 +507,7 @@ class CheckRefs: for fp, ffp in sorted(self.find_files("art/variants", "xml")): self.files.append(str(fp)) self.roots.append(str(fp)) - variant = ElementTree.parse(ffp).getroot() + variant = ET.parse(ffp).getroot() self.append_variant_dependencies(variant, fp) def add_art(self): @@ -543,7 +550,7 @@ class CheckRefs: self.logger.info("Loading materials...") for fp, ffp in sorted(self.find_files("art/materials", "xml")): self.files.append(str(fp)) - material_elem = ElementTree.parse(ffp).getroot() + material_elem = ET.parse(ffp).getroot() for alternative in material_elem.findall("alternative"): material = alternative.get("material") if material is not None: @@ -554,7 +561,7 @@ class CheckRefs: for fp, ffp in sorted(self.find_files("art/particles", "xml")): self.files.append(str(fp)) self.roots.append(str(fp)) - particle = ElementTree.parse(ffp).getroot() + particle = ET.parse(ffp).getroot() texture = particle.find("texture") if texture is not None: self.deps.append((str(fp), texture.text)) @@ -564,7 +571,7 @@ class CheckRefs: for fp, ffp in sorted(self.find_files("audio", "xml")): self.files.append(str(fp)) self.roots.append(str(fp)) - sound_group = ElementTree.parse(ffp).getroot() + sound_group = ET.parse(ffp).getroot() path = sound_group.find("Path").text.rstrip("/") for sound in sound_group.findall("Sound"): self.deps.append((str(fp), f"{path}/{sound.text}")) @@ -614,7 +621,7 @@ class CheckRefs: # GUI page definitions are assumed to be named page_[something].xml and alone in that. if match(r".*[\\\/]page(_[^.\/\\]+)?\.xml$", str(fp)): self.roots.append(str(fp)) - root_xml = ElementTree.parse(ffp).getroot() + root_xml = ET.parse(ffp).getroot() for include in root_xml.findall("include"): # If including an entire directory, find all the *.xml files if include.text.endswith("/"): @@ -629,7 +636,7 @@ class CheckRefs: else: self.deps.append((str(fp), f"gui/{include.text}")) else: - xml = ElementTree.parse(ffp) + xml = ET.parse(ffp) root_xml = xml.getroot() name = root_xml.tag self.roots.append(str(fp)) @@ -662,7 +669,6 @@ class CheckRefs: if style.get("sound_disabled"): self.deps.append((str(fp), f"{style.get('sound_disabled')}")) # TODO: look at sprites, styles, etc - pass elif name == "sprites": for sprite in root_xml.findall("sprite"): for image in sprite.findall("image"): @@ -711,7 +717,7 @@ class CheckRefs: def add_tips(self): self.logger.info("Loading tips...") - for fp, ffp in sorted(self.find_files("gui/text/tips", "txt")): + for fp, _ffp in sorted(self.find_files("gui/text/tips", "txt")): relative_path = str(fp) self.files.append(relative_path) self.roots.append(relative_path) @@ -770,7 +776,7 @@ class CheckRefs: continue self.files.append(str(fp)) self.roots.append(str(fp)) - terrain = ElementTree.parse(ffp).getroot() + terrain = ET.parse(ffp).getroot() for texture in terrain.find("textures").findall("texture"): if texture.get("file"): self.deps.append((str(fp), f"art/textures/terrain/{texture.get('file')}")) @@ -796,7 +802,7 @@ class CheckRefs: uniq_files = set(self.files) uniq_files = [r.replace(sep, "/") for r in uniq_files] lower_case_files = {f.lower(): f for f in uniq_files} - reverse_deps = dict() + reverse_deps = {} for parent, dep in self.deps: if sep != "/": parent = parent.replace(sep, "/") @@ -817,16 +823,18 @@ class CheckRefs: continue callers = [str(self.vfs_to_relative_to_mods(ref)) for ref in reverse_deps[dep]] - self.logger.error(f"Missing file '{dep}' referenced by: {', '.join(sorted(callers))}") + self.logger.error( + "Missing file '%s' referenced by: %s", dep, ", ".join(sorted(callers)) + ) self.inError = True if dep.lower() in lower_case_files: self.logger.warning( - f"### Case-insensitive match (found '{lower_case_files[dep.lower()]}')" + "### Case-insensitive match (found '%s')", lower_case_files[dep.lower()] ) def check_unused(self): self.logger.info("Looking for unused files...") - deps = dict() + deps = {} for parent, dep in self.deps: if sep != "/": parent = parent.replace(sep, "/") @@ -859,7 +867,7 @@ class CheckRefs: ) ): continue - self.logger.warning(f"Unused file '{str(self.vfs_to_relative_to_mods(f))}'") + self.logger.warning("Unused file '%s'", str(self.vfs_to_relative_to_mods(f))) if __name__ == "__main__": diff --git a/source/tools/entity/creationgraph.py b/source/tools/entity/creationgraph.py old mode 100644 new mode 100755 index 7a053df5cf..180c937ea1 --- a/source/tools/entity/creationgraph.py +++ b/source/tools/entity/creationgraph.py @@ -4,7 +4,8 @@ from pathlib import Path from re import split from subprocess import run from sys import exit -from scriptlib import warn, SimulTemplateEntity, find_files + +from scriptlib import SimulTemplateEntity, find_files, warn def find_entities(vfs_root): @@ -56,8 +57,12 @@ def main(): warn(f"Invalid TrainingQueue reference: {f} -> {training_queue}") dot_f.write(f'"{f}" -> "{training_queue}" [color=blue];\n') dot_f.write("}\n") - if run(["dot", "-V"], capture_output=True).returncode == 0: - exit(run(["dot", "-Tpng", "creation.dot", "-o", "creation.png"], text=True).returncode) + if run(["dot", "-V"], capture_output=True, check=False).returncode == 0: + exit( + run( + ["dot", "-Tpng", "creation.dot", "-o", "creation.png"], text=True, check=False + ).returncode + ) if __name__ == "__main__": diff --git a/source/tools/entity/entvalidate.py b/source/tools/entity/entvalidate.py old mode 100644 new mode 100755 index 5d3b2faafc..ba07f11358 --- a/source/tools/entity/entvalidate.py +++ b/source/tools/entity/entvalidate.py @@ -1,16 +1,18 @@ #!/usr/bin/env python3 +from __future__ import annotations + import argparse import logging -from pathlib import Path import shutil -from subprocess import run, CalledProcessError import sys +from pathlib import Path +from subprocess import CalledProcessError, run from typing import Sequence - -from xml.etree import ElementTree +from xml.etree import ElementTree as ET from scriptlib import SimulTemplateEntity, find_files + SIMUL_TEMPLATES_PATH = Path("simulation/templates") ENTITY_RELAXNG_FNAME = "entity.rng" RELAXNG_SCHEMA_ERROR_MSG = """Relax NG schema non existant. @@ -30,8 +32,7 @@ class SingleLevelFilter(logging.Filter): def filter(self, record): if self.reject: return record.levelno != self.passlevel - else: - return record.levelno == self.passlevel + return record.levelno == self.passlevel logger = logging.getLogger(__name__) @@ -96,18 +97,21 @@ def main(argv: Sequence[str] | None = None) -> int: continue path = fp.as_posix() - if path.startswith(f"{SIMUL_TEMPLATES_PATH.as_posix()}/mixins/") or path.startswith( - f"{SIMUL_TEMPLATES_PATH.as_posix()}/special/" + if path.startswith( + ( + f"{SIMUL_TEMPLATES_PATH.as_posix()}/mixins/", + f"{SIMUL_TEMPLATES_PATH.as_posix()}/special/", + ) ): continue if args.verbose: - logger.info(f"Parsing {fp}...") + logger.info("Parsing %s...", fp) count += 1 entity = simul_template_entity.load_inherited( SIMUL_TEMPLATES_PATH, str(fp.relative_to(SIMUL_TEMPLATES_PATH)), [args.mod_name] ) - xmlcontent = ElementTree.tostring(entity, encoding="unicode") + xmlcontent = ET.tostring(entity, encoding="unicode") try: run( ["xmllint", "--relaxng", str(args.relaxng_schema.resolve()), "-"], @@ -120,11 +124,11 @@ def main(argv: Sequence[str] | None = None) -> int: except CalledProcessError as e: failed += 1 if e.stderr: - logger.error(e.stderr) + logger.exception(e.stderr) if e.stdout: logger.info(e.stdout) - logger.info(f"Total: {count}; failed: {failed}") + logger.info("Total: %s; failed: %s", count, failed) return 0 diff --git a/source/tools/entity/scriptlib/__init__.py b/source/tools/entity/scriptlib/__init__.py index d6e00a519e..e69ba9e1a5 100644 --- a/source/tools/entity/scriptlib/__init__.py +++ b/source/tools/entity/scriptlib/__init__.py @@ -1,8 +1,8 @@ from collections import Counter from decimal import Decimal -from re import split -from xml.etree import ElementTree from os.path import exists +from re import split +from xml.etree import ElementTree as ET class SimulTemplateEntity: @@ -77,7 +77,7 @@ class SimulTemplateEntity: base_tag.remove(base_child) base_child = None if base_child is None: - base_child = ElementTree.Element(child.tag) + base_child = ET.Element(child.tag) base_tag.append(base_child) self.apply_layer(base_child, child) if "replace" in base_child.attrib: @@ -95,28 +95,27 @@ class SimulTemplateEntity: if "|" in vfs_path: paths = vfs_path.split("|", 1) base = self._load_inherited(base_path, paths[1], mods, base) - base = self._load_inherited(base_path, paths[0], mods, base) - return base + return self._load_inherited(base_path, paths[0], mods, base) main_mod = self.get_main_mod(base_path, vfs_path, mods) fp = self.get_file(base_path, vfs_path, main_mod) - layer = ElementTree.parse(fp).getroot() + layer = ET.parse(fp).getroot() for el in layer.iter(): children = [x.tag for x in el] duplicates = [x for x, c in Counter(children).items() if c > 1] if duplicates: for dup in duplicates: - self.logger.warning(f"Duplicate child node '{dup}' in tag {el.tag} of {fp}") + self.logger.warning( + "Duplicate child node '%s' in tag %s of %s", dup, el.tag, fp + ) if layer.get("parent"): parent = self._load_inherited(base_path, layer.get("parent"), mods, base) self.apply_layer(parent, layer) return parent - else: - if not base: - return layer - else: - self.apply_layer(base, layer) - return base + if not base: + return layer + self.apply_layer(base, layer) + return base def find_files(vfs_root, mods, vfs_path, *ext_list): @@ -130,7 +129,7 @@ def find_files(vfs_root, mods, vfs_path, *ext_list): def find_recursive(dp, base): """(relative Path, full Path) generator""" if dp.is_dir(): - if dp.name != ".svn" and dp.name != ".git" and not dp.name.endswith("~"): + if dp.name not in (".svn", ".git") and not dp.name.endswith("~"): for fp in dp.iterdir(): yield from find_recursive(fp, base) elif dp.suffix in full_exts: diff --git a/source/tools/fontbuilder2/FontLoader.py b/source/tools/fontbuilder2/FontLoader.py index 35b2339fe2..2eb92b8bd9 100644 --- a/source/tools/fontbuilder2/FontLoader.py +++ b/source/tools/fontbuilder2/FontLoader.py @@ -1,9 +1,11 @@ # Adapted from http://cairographics.org/freetypepython/ import ctypes -import cairo import sys +import cairo + + CAIRO_STATUS_SUCCESS = 0 FT_Err_Ok = 0 @@ -58,11 +60,11 @@ def create_cairo_font_face_for_file(filename, faceindex=0, loadoptions=0): # create cairo font face for freetype face cr_face = _cairo_so.cairo_ft_font_face_create_for_ft_face(ft_face, loadoptions) - if CAIRO_STATUS_SUCCESS != _cairo_so.cairo_font_face_status(cr_face): + if _cairo_so.cairo_font_face_status(cr_face) != CAIRO_STATUS_SUCCESS: raise Exception("Error creating cairo font face for " + filename) _cairo_so.cairo_set_font_face(cairo_t, cr_face) - if CAIRO_STATUS_SUCCESS != _cairo_so.cairo_status(cairo_t): + if _cairo_so.cairo_status(cairo_t) != CAIRO_STATUS_SUCCESS: raise Exception("Error creating cairo font face for " + filename) face = cairo_ctx.get_font_face() diff --git a/source/tools/fontbuilder2/Packer.py b/source/tools/fontbuilder2/Packer.py index 5c2afb44f1..40a1758439 100644 --- a/source/tools/fontbuilder2/Packer.py +++ b/source/tools/fontbuilder2/Packer.py @@ -23,7 +23,7 @@ class OutOfSpaceError(Exception): pass -class Point(object): +class Point: def __init__(self, x, y): self.x = x self.y = y @@ -33,7 +33,7 @@ class Point(object): return self.x - other.x -class RectanglePacker(object): +class RectanglePacker: """Base class for rectangle packing algorithms By uniting all rectangle packers under this common base class, you can @@ -41,13 +41,15 @@ class RectanglePacker(object): performant one for a given job. An almost exhaustive list of packing algorithms can be found here: - http://www.csc.liv.ac.uk/~epa/surveyhtml.html""" + http://www.csc.liv.ac.uk/~epa/surveyhtml.html + """ def __init__(self, packingAreaWidth, packingAreaHeight): """Initializes a new rectangle packer packingAreaWidth: Maximum width of the packing area - packingAreaHeight: Maximum height of the packing area""" + packingAreaHeight: Maximum height of the packing area + """ self.packingAreaWidth = packingAreaWidth self.packingAreaHeight = packingAreaHeight @@ -57,7 +59,8 @@ class RectanglePacker(object): rectangleWidth: Width of the rectangle to allocate rectangleHeight: Height of the rectangle to allocate - Returns the location at which the rectangle has been placed""" + Returns the location at which the rectangle has been placed + """ point = self.TryPack(rectangleWidth, rectangleHeight) if not point: @@ -72,7 +75,8 @@ class RectanglePacker(object): rectangleHeight: Height of the rectangle to allocate Returns a Point instance if space for the rectangle could be allocated - be found, otherwise returns None""" + be found, otherwise returns None + """ raise NotImplementedError @@ -112,13 +116,15 @@ class CygonRectanglePacker(RectanglePacker): To quickly discover these locations, the packer uses a sophisticated data structure that stores the upper silhouette of the packing area. When a new rectangle needs to be added, only the silouette edges need to be - analyzed to find the position where the rectangle would achieve the lowest""" + analyzed to find the position where the rectangle would achieve the lowest + """ def __init__(self, packingAreaWidth, packingAreaHeight): """Initializes a new rectangle packer packingAreaWidth: Maximum width of the packing area - packingAreaHeight: Maximum height of the packing area""" + packingAreaHeight: Maximum height of the packing area + """ RectanglePacker.__init__(self, packingAreaWidth, packingAreaHeight) # Stores the height silhouette of the rectangles @@ -134,7 +140,8 @@ class CygonRectanglePacker(RectanglePacker): rectangleHeight: Height of the rectangle to allocate Returns a Point instance if space for the rectangle could be allocated - be found, otherwise returns None""" + be found, otherwise returns None + """ placement = None # If the rectangle is larger than the packing area in any dimension, @@ -159,7 +166,8 @@ class CygonRectanglePacker(RectanglePacker): rectangleHeight: Height of the rectangle to find a position for Returns a Point instance if a valid placement for the rectangle could - be found, otherwise returns None""" + be found, otherwise returns None + """ # Slice index, vertical position and score of the best placement we # could find bestSliceIndex = -1 # Slice index where the best placement was found @@ -181,8 +189,7 @@ class CygonRectanglePacker(RectanglePacker): # any lower than this without overlapping the other rectangles. highest = self.heightSlices[leftSliceIndex].y for index in range(leftSliceIndex + 1, rightSliceIndex): - if self.heightSlices[index].y > highest: - highest = self.heightSlices[index].y + highest = max(self.heightSlices[index].y, highest) # Only process this position if it doesn't leave the packing area if highest + rectangleHeight < self.packingAreaHeight: @@ -224,15 +231,15 @@ class CygonRectanglePacker(RectanglePacker): # could be found. if bestSliceIndex == -1: return None - else: - return Point(self.heightSlices[bestSliceIndex].x, bestSliceY) + return Point(self.heightSlices[bestSliceIndex].x, bestSliceY) def integrateRectangle(self, left, width, bottom): """Integrates a new rectangle into the height slice table left: Position of the rectangle's left side width: Width of the rectangle - bottom: Position of the rectangle's lower side""" + bottom: Position of the rectangle's lower side + """ # Find the first slice that is touched by the rectangle startSlice = bisect_left(self.heightSlices, Point(left, 0)) diff --git a/source/tools/fontbuilder2/__init__.py b/source/tools/fontbuilder2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/tools/fontbuilder2/dumpfontchars.py b/source/tools/fontbuilder2/dumpfontchars.py index 52f8fc04db..3586663812 100644 --- a/source/tools/fontbuilder2/dumpfontchars.py +++ b/source/tools/fontbuilder2/dumpfontchars.py @@ -7,7 +7,7 @@ import FontLoader def dump_font(ttf): (face, indexes) = FontLoader.create_cairo_font_face_for_file( - "../../../binaries/data/tools/fontbuilder/fonts/%s" % ttf, 0, FontLoader.FT_LOAD_DEFAULT + f"../../../binaries/data/tools/fontbuilder/fonts/{ttf}", 0, FontLoader.FT_LOAD_DEFAULT ) mappings = [(c, indexes(chr(c))) for c in range(1, 65535)] diff --git a/source/tools/fontbuilder2/fontbuilder.py b/source/tools/fontbuilder2/fontbuilder.py old mode 100644 new mode 100755 index 98c81392dd..1d5d207520 --- a/source/tools/fontbuilder2/fontbuilder.py +++ b/source/tools/fontbuilder2/fontbuilder.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 -import cairo import codecs import math +import cairo import FontLoader import Packer # Representation of a rendered glyph -class Glyph(object): +class Glyph: def __init__(self, ctx, renderstyle, char, idx, face, size): self.renderstyle = renderstyle self.char = char @@ -18,7 +18,7 @@ class Glyph(object): self.size = size self.glyph = (idx, 0, 0) - if not ctx.get_font_face() == self.face: + if ctx.get_font_face() != self.face: ctx.set_font_face(self.face) ctx.set_font_size(self.size) extents = ctx.glyph_extents([self.glyph]) @@ -31,7 +31,7 @@ class Glyph(object): bb = [inf, inf, -inf, -inf] if "stroke" in self.renderstyle: - for c, w in self.renderstyle["stroke"]: + for _c, w in self.renderstyle["stroke"]: ctx.set_line_width(w) ctx.glyph_path([self.glyph]) e = ctx.stroke_extents() @@ -60,7 +60,7 @@ class Glyph(object): self.pos = packer.Pack(self.w, self.h) def render(self, ctx): - if not ctx.get_font_face() == self.face: + if ctx.get_font_face() != self.face: ctx.set_font_face(self.face) ctx.set_font_size(self.size) ctx.save() @@ -107,7 +107,7 @@ def generate_font(outname, ttfNames, loadopts, size, renderstyle, dsizes): indexList = [] for i in range(len(ttfNames)): (face, indices) = FontLoader.create_cairo_font_face_for_file( - "../../../binaries/data/tools/fontbuilder/fonts/%s" % ttfNames[i], 0, loadopts + f"../../../binaries/data/tools/fontbuilder/fonts/{ttfNames[i]}", 0, loadopts ) faceList.append(face) if ttfNames[i] not in dsizes: @@ -166,10 +166,10 @@ def generate_font(outname, ttfNames, loadopts, size, renderstyle, dsizes): ctx, surface = setup_context(w, h, renderstyle) for g in glyphs: g.render(ctx) - surface.write_to_png("%s.png" % outname) + surface.write_to_png(f"{outname}.png") # Output the .fnt file with all the glyph positions etc - fnt = open("%s.fnt" % outname, "w") + fnt = open(f"{outname}.fnt", "w") fnt.write("101\n") fnt.write("%d %d\n" % (w, h)) fnt.write("%s\n" % ("rgba" if "colour" in renderstyle else "a")) @@ -249,7 +249,7 @@ fonts = ( ) for name, (fontnames, loadopts), size, style in fonts: - print("%s..." % name) + print(f"{name}...") generate_font( - "../../../binaries/data/mods/mod/fonts/%s" % name, fontnames, loadopts, size, style, dsizes + f"../../../binaries/data/mods/mod/fonts/{name}", fontnames, loadopts, size, style, dsizes ) diff --git a/source/tools/i18n/checkDiff.py b/source/tools/i18n/checkDiff.py old mode 100644 new mode 100755 index 1e7ff7a22d..2f84061baa --- a/source/tools/i18n/checkDiff.py +++ b/source/tools/i18n/checkDiff.py @@ -30,18 +30,18 @@ def get_diff(): """Return a diff using svn diff""" os.chdir(projectRootDirectory) - diff_process = subprocess.run(["svn", "diff", "binaries"], capture_output=True) + diff_process = subprocess.run(["svn", "diff", "binaries"], capture_output=True, check=False) if diff_process.returncode != 0: print(f"Error running svn diff: {diff_process.stderr.decode('utf-8')}. Exiting.") - return + return None return io.StringIO(diff_process.stdout.decode("utf-8")) def check_diff(diff: io.StringIO, verbose=False) -> List[str]: """Run through a diff of .po files and check that some of the changes are real translations changes and not just noise (line changes....). - The algorithm isn't extremely clever, but it is quite fast.""" - + The algorithm isn't extremely clever, but it is quite fast. + """ keep = set() files = set() @@ -85,10 +85,11 @@ def check_diff(diff: io.StringIO, verbose=False) -> List[str]: def revert_files(files: List[str], verbose=False): - revert_process = subprocess.run(["svn", "revert"] + files, capture_output=True) + revert_process = subprocess.run(["svn", "revert", *files], capture_output=True, check=False) if revert_process.returncode != 0: print( - f"Warning: Some files could not be reverted. Error: {revert_process.stderr.decode('utf-8')}" + "Warning: Some files could not be reverted. " + f"Error: {revert_process.stderr.decode('utf-8')}" ) if verbose: for file in files: @@ -97,7 +98,7 @@ def revert_files(files: List[str], verbose=False): def add_untracked(verbose=False): """Add untracked .po files to svn""" - diff_process = subprocess.run(["svn", "st", "binaries"], capture_output=True) + diff_process = subprocess.run(["svn", "st", "binaries"], capture_output=True, check=False) if diff_process.stderr != b"": print(f"Error running svn st: {diff_process.stderr.decode('utf-8')}. Exiting.") return @@ -110,7 +111,9 @@ def add_untracked(verbose=False): file = line[1:].strip() if not file.endswith(".po") and not file.endswith(".pot"): continue - add_process = subprocess.run(["svn", "add", file, "--parents"], capture_output=True) + add_process = subprocess.run( + ["svn", "add", file, "--parents"], capture_output=True, check=False + ) if add_process.stderr != b"": print(f"Warning: file {file} could not be added.") if verbose: diff --git a/source/tools/i18n/checkTranslations.py b/source/tools/i18n/checkTranslations.py old mode 100644 new mode 100755 index 94178186b3..b8add3e1f3 --- a/source/tools/i18n/checkTranslations.py +++ b/source/tools/i18n/checkTranslations.py @@ -16,15 +16,16 @@ # You should have received a copy of the GNU General Public License # along with 0 A.D. If not, see . -import sys +import multiprocessing import os import re -import multiprocessing +import sys from i18n_helper import l10nFolderName, projectRootDirectory from i18n_helper.catalog import Catalog from i18n_helper.globber import getCatalogs + VERBOSE = 0 @@ -49,7 +50,8 @@ class MessageChecker: pluralUrls = set(self.regex.findall(templateMessage.id[1])) if pluralUrls.difference(patterns): print( - f"{inputFilePath} - Different {self.human_name} in singular and plural source strings " + f"{inputFilePath} - Different {self.human_name} in " + f"singular and plural source strings " f"for '{templateMessage}' in '{inputFilePath}'" ) @@ -71,8 +73,10 @@ class MessageChecker: if unknown_patterns: print( f'{inputFilePath} - {translationCatalog.locale}: ' - f'Found unknown {self.human_name} {", ".join(["`" + x + "`" for x in unknown_patterns])} in the translation ' - f'which do not match any of the URLs in the template: {", ".join(["`" + x + "`" for x in patterns])}' + f'Found unknown {self.human_name} ' + f'{", ".join(["`" + x + "`" for x in unknown_patterns])} ' + f'in the translation which do not match any of the URLs ' + f'in the template: {", ".join(["`" + x + "`" for x in patterns])}' ) if templateMessage.pluralizable and translationMessage.pluralizable: @@ -84,8 +88,11 @@ class MessageChecker: if unknown_patterns_multi: print( f'{inputFilePath} - {translationCatalog.locale}: ' - f'Found unknown {self.human_name} {", ".join(["`" + x + "`" for x in unknown_patterns_multi])} in the pluralised translation ' - f'which do not match any of the URLs in the template: {", ".join(["`" + x + "`" for x in pluralUrls])}' + f'Found unknown {self.human_name} ' + f'{", ".join(["`" + x + "`" for x in unknown_patterns_multi])} ' + f'in the pluralised translation which do not ' + f'match any of the URLs in the template: ' + f'{", ".join(["`" + x + "`" for x in pluralUrls])}' ) @@ -123,7 +130,7 @@ def main(): "before you run this script.\n\tPOT files are not in the repository.\n" ) foundPots = 0 - for root, folders, filenames in os.walk(projectRootDirectory): + for root, _folders, filenames in os.walk(projectRootDirectory): for filename in filenames: if ( len(filename) > 4 diff --git a/source/tools/i18n/cleanTranslationFiles.py b/source/tools/i18n/cleanTranslationFiles.py old mode 100644 new mode 100755 index c807d38fdf..186c8e6072 --- a/source/tools/i18n/cleanTranslationFiles.py +++ b/source/tools/i18n/cleanTranslationFiles.py @@ -26,13 +26,13 @@ However that needs to be fixed on the transifex side, see rP25896. For now strip the e-mails using this script. """ -import sys -import os -import glob -import re import fileinput +import glob +import os +import re +import sys -from i18n_helper import l10nFolderName, transifexClientFolder, projectRootDirectory +from i18n_helper import l10nFolderName, projectRootDirectory, transifexClientFolder def main(): diff --git a/source/tools/i18n/creditTranslators.py b/source/tools/i18n/creditTranslators.py index 0e15d9408d..c9ff27a0cd 100755 --- a/source/tools/i18n/creditTranslators.py +++ b/source/tools/i18n/creditTranslators.py @@ -36,11 +36,11 @@ from collections import defaultdict from pathlib import Path from babel import Locale, UnknownLocaleError +from i18n_helper import l10nFolderName, projectRootDirectory, transifexClientFolder -from i18n_helper import l10nFolderName, transifexClientFolder, projectRootDirectory poLocations = [] -for root, folders, filenames in os.walk(projectRootDirectory): +for root, folders, _filenames in os.walk(projectRootDirectory): for folder in folders: if folder == l10nFolderName: if os.path.exists(os.path.join(root, folder, transifexClientFolder)): @@ -78,7 +78,7 @@ for location in poLocations: lang = file.stem.split(".")[0] # Skip debug translations - if lang == "debug" or lang == "long": + if lang in ("debug", "long"): continue with file.open(encoding="utf-8") as poFile: @@ -98,12 +98,10 @@ for location in poLocations: # Sort translator names and remove duplicates # Sorting should ignore case, but prefer versions of names starting # with an upper case letter to have a neat credits list. -for lang in langsLists.keys(): +for lang in langsLists: translators = {} for name in sorted(langsLists[lang], reverse=True): - if name.lower() not in translators.keys(): - translators[name.lower()] = name - elif name.istitle(): + if name.lower() not in translators or name.istitle(): translators[name.lower()] = name langsLists[lang] = sorted(translators.values(), key=lambda s: s.lower()) diff --git a/source/tools/i18n/extractors/extractors.py b/source/tools/i18n/extractors/extractors.py old mode 100644 new mode 100755 index 54cffc62a6..f1bccc6f4f --- a/source/tools/i18n/extractors/extractors.py +++ b/source/tools/i18n/extractors/extractors.py @@ -3,29 +3,33 @@ # Copyright (C) 2024 Wildfire Games. # All rights reserved. # -# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -# following conditions are met: # -# Redistributions of source code must retain the above copyright notice, this list of conditions and the following -# disclaimer. -# Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided with the distribution. -# The name of the author may not be used to endorse or promote products derived from this software without specific -# prior written permission. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. # -# THIS SOFTWARE IS PROVIDED BY THE AUTHOR “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR “AS IS” AND ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import codecs -import re -import os -import sys import json as jsonParser - +import os +import re +import sys from textwrap import dedent @@ -51,7 +55,7 @@ def pathmatch(mask, path): return re.match(p, path) is not None -class Extractor(object): +class Extractor: def __init__(self, directoryPath, filemasks, options): self.directoryPath = directoryPath self.options = options @@ -66,14 +70,15 @@ class Extractor(object): def run(self): """Extracts messages. - :return: An iterator over ``(message, plural, context, (location, pos), comment)`` tuples. + :return: An iterator over ``(message, plural, context, (location, pos), comment)`` + tuples. :rtype: ``iterator`` """ empty_string_pattern = re.compile(r"^\s*$") directoryAbsolutePath = os.path.abspath(self.directoryPath) for root, folders, filenames in os.walk(directoryAbsolutePath): for subdir in folders: - if subdir.startswith(".") or subdir.startswith("_"): + if subdir.startswith((".", "_")): folders.remove(subdir) folders.sort() filenames.sort() @@ -108,7 +113,6 @@ class Extractor(object): :return: An iterator over ``(message, plural, context, position, comments)`` tuples. :rtype: ``iterator`` """ - pass class javascript(Extractor): @@ -134,8 +138,7 @@ class javascript(Extractor): for token in tokenize(fileObject.read(), dotted=False): if token.type == "operator" and ( - token.value == "(" - or (call_stack != -1 and (token.value == "[" or token.value == "{")) + token.value == "(" or (call_stack != -1 and (token.value in ("[", "{"))) ): if funcname: message_lineno = token.lineno @@ -215,11 +218,7 @@ class javascript(Extractor): elif token.value == "+": concatenate_next = True - elif ( - call_stack > 0 - and token.type == "operator" - and (token.value == ")" or token.value == "]" or token.value == "}") - ): + elif call_stack > 0 and token.type == "operator" and (token.value in (")", "]", "}")): call_stack -= 1 elif funcname and call_stack == -1: @@ -242,10 +241,7 @@ class javascript(Extractor): def extractFromFile(self, filepath): with codecs.open(filepath, "r", encoding="utf-8-sig") as fileObject: for lineno, funcname, messages, comments in self.extractJavascriptFromFile(fileObject): - if funcname: - spec = self.options.get("keywords", {})[funcname] or (1,) - else: - spec = (1,) + spec = self.options.get("keywords", {})[funcname] or (1,) if funcname else (1,) if not isinstance(messages, (list, tuple)): messages = [messages] if not messages: @@ -300,8 +296,6 @@ class javascript(Extractor): class cpp(javascript): """Extract messages from C++ source code.""" - pass - class txt(Extractor): """Extract messages from plain text files.""" @@ -318,8 +312,12 @@ class txt(Extractor): class json(Extractor): """Extract messages from JSON files.""" - def __init__(self, directoryPath=None, filemasks=[], options={}): - super(json, self).__init__(directoryPath, filemasks, options) + def __init__(self, directoryPath=None, filemasks=None, options=None): + if options is None: + options = {} + if filemasks is None: + filemasks = [] + super().__init__(directoryPath, filemasks, options) self.keywords = self.options.get("keywords", {}) self.context = self.options.get("context", None) self.comments = self.options.get("comments", []) @@ -347,7 +345,8 @@ class json(Extractor): yield message, context else: raise Exception( - "Unexpected JSON document parent structure (not a list or a dictionary). You must extend the JSON extractor to support it." + "Unexpected JSON document parent structure (not a list or a dictionary). " + "You must extend the JSON extractor to support it." ) def parseList(self, itemsList): @@ -431,8 +430,7 @@ class json(Extractor): if isinstance(dictionary[innerKeyword], str): yield self.extractString(dictionary[innerKeyword], keyword) elif isinstance(dictionary[innerKeyword], list): - for message, context in self.extractList(dictionary[innerKeyword], keyword): - yield message, context + yield from self.extractList(dictionary[innerKeyword], keyword) elif isinstance(dictionary[innerKeyword], dict): extract = self.extractDictionary(dictionary[innerKeyword], keyword) if extract: @@ -443,7 +441,7 @@ class xml(Extractor): """Extract messages from XML files.""" def __init__(self, directoryPath, filemasks, options): - super(xml, self).__init__(directoryPath, filemasks, options) + super().__init__(directoryPath, filemasks, options) self.keywords = self.options.get("keywords", {}) self.jsonExtractor = None @@ -483,7 +481,9 @@ class xml(Extractor): comments.append(comment) if "splitOnWhitespace" in self.keywords[keyword]: for splitText in element.text.split(): - # split on whitespace is used for token lists, there, a leading '-' means the token has to be removed, so it's not to be processed here either + # split on whitespace is used for token lists, there, a + # leading '-' means the token has to be removed, so it's not + # to be processed here either if splitText[0] != "-": yield str(splitText), None, context, lineno, comments else: @@ -491,7 +491,7 @@ class xml(Extractor): # Hack from http://stackoverflow.com/a/2819788 -class FakeSectionHeader(object): +class FakeSectionHeader: def __init__(self, fp): self.fp = fp self.sechead = "[root]\n" @@ -510,7 +510,7 @@ class ini(Extractor): """Extract messages from INI files.""" def __init__(self, directoryPath, filemasks, options): - super(ini, self).__init__(directoryPath, filemasks, options) + super().__init__(directoryPath, filemasks, options) self.keywords = self.options.get("keywords", []) def extractFromFile(self, filepath): diff --git a/source/tools/i18n/generateDebugTranslation.py b/source/tools/i18n/generateDebugTranslation.py old mode 100644 new mode 100755 index dc642209cb..32937e1a86 --- a/source/tools/i18n/generateDebugTranslation.py +++ b/source/tools/i18n/generateDebugTranslation.py @@ -17,9 +17,9 @@ # along with 0 A.D. If not, see . import argparse +import multiprocessing import os import sys -import multiprocessing from i18n_helper import l10nFolderName, projectRootDirectory from i18n_helper.catalog import Catalog @@ -179,9 +179,10 @@ def main(): if found_pot_files == 0: print( - "This script did not work because no ‘.pot’ files were found. " - "Please, run ‘updateTemplates.py’ to generate the ‘.pot’ files, and run ‘pullTranslations.py’ to pull the latest translations from Transifex. " - "Then you can run this script to generate ‘.po’ files with obvious debug strings." + "This script did not work because no '.pot' files were found. " + "Please, run 'updateTemplates.py' to generate the '.pot' files, and run " + "'pullTranslations.py' to pull the latest translations from Transifex. " + "Then you can run this script to generate '.po' files with obvious debug strings." ) diff --git a/source/tools/i18n/i18n_helper/__init__.py b/source/tools/i18n/i18n_helper/__init__.py index 66d3f60c92..ef1d3c3298 100644 --- a/source/tools/i18n/i18n_helper/__init__.py +++ b/source/tools/i18n/i18n_helper/__init__.py @@ -1,5 +1,6 @@ import os + l10nFolderName = "l10n" transifexClientFolder = ".tx" l10nToolsDirectory = os.path.dirname(os.path.realpath(__file__)) diff --git a/source/tools/i18n/i18n_helper/catalog.py b/source/tools/i18n/i18n_helper/catalog.py index 6410685fd7..e2eedcb253 100644 --- a/source/tools/i18n/i18n_helper/catalog.py +++ b/source/tools/i18n/i18n_helper/catalog.py @@ -41,7 +41,7 @@ class Catalog(BabelCatalog): }: headers.append((name, value)) - return [("Project-Id-Version", self._project)] + headers + return [("Project-Id-Version", self._project), *headers] @staticmethod def readFrom(file_path, locale=None): diff --git a/source/tools/i18n/i18n_helper/globber.py b/source/tools/i18n/i18n_helper/globber.py index 2108ee4b5b..20dd7402bf 100644 --- a/source/tools/i18n/i18n_helper/globber.py +++ b/source/tools/i18n/i18n_helper/globber.py @@ -1,12 +1,12 @@ """Utils to list .po""" import os -from typing import List +from typing import List, Optional from i18n_helper.catalog import Catalog -def getCatalogs(inputFilePath, filters: List[str] = None) -> List[Catalog]: +def getCatalogs(inputFilePath, filters: Optional[List[str]] = None) -> List[Catalog]: """Returns a list of "real" catalogs (.po) in the given folder.""" existingTranslationCatalogs = [] l10nFolderPath = os.path.dirname(inputFilePath) diff --git a/source/tools/i18n/pullTranslations.py b/source/tools/i18n/pullTranslations.py old mode 100644 new mode 100755 index 7a85711c8f..9952375520 --- a/source/tools/i18n/pullTranslations.py +++ b/source/tools/i18n/pullTranslations.py @@ -19,7 +19,7 @@ import os import subprocess -from i18n_helper import l10nFolderName, transifexClientFolder, projectRootDirectory +from i18n_helper import l10nFolderName, projectRootDirectory, transifexClientFolder def main(): @@ -30,7 +30,7 @@ def main(): path = os.path.join(root, folder) os.chdir(path) print(f"INFO: Starting to pull translations in {path}...") - subprocess.run(["tx", "pull", "-a", "-f"]) + subprocess.run(["tx", "pull", "-a", "-f"], check=False) if __name__ == "__main__": diff --git a/source/tools/i18n/tests/__init__.py b/source/tools/i18n/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/tools/i18n/tests/test_checkDiff.py b/source/tools/i18n/tests/test_checkDiff.py index 217bce45cc..550d236193 100644 --- a/source/tools/i18n/tests/test_checkDiff.py +++ b/source/tools/i18n/tests/test_checkDiff.py @@ -1,7 +1,9 @@ import io + import pytest from checkDiff import check_diff + PATCHES = [ """ Index: binaries/data/l10n/en_GB.engine.po diff --git a/source/tools/i18n/updateTemplates.py b/source/tools/i18n/updateTemplates.py old mode 100644 new mode 100755 index 83e8a33d43..88d5e90e12 --- a/source/tools/i18n/updateTemplates.py +++ b/source/tools/i18n/updateTemplates.py @@ -17,14 +17,14 @@ # along with 0 A.D. If not, see . import json -import os import multiprocessing +import os from importlib import import_module - from i18n_helper import l10nFolderName, projectRootDirectory from i18n_helper.catalog import Catalog + messagesFilename = "messages.json" @@ -38,32 +38,25 @@ def warnAboutUntouchedMods(): if modFolder[0] != "_" and modFolder[0] != ".": if not os.path.exists(os.path.join(modsRootFolder, modFolder, l10nFolderName)): untouchedMods[modFolder] = ( - "There is no '{folderName}' folder in the root folder of this mod.".format( - folderName=l10nFolderName - ) + f"There is no '{l10nFolderName}' folder in the root folder of this mod." ) elif not os.path.exists( os.path.join(modsRootFolder, modFolder, l10nFolderName, messagesFilename) ): untouchedMods[modFolder] = ( - "There is no '{filename}' file within the '{folderName}' folder in the root folder of this mod.".format( - folderName=l10nFolderName, filename=messagesFilename - ) + f"There is no '{messagesFilename}' file within the '{l10nFolderName}' folder " + f"in the root folder of this mod." ) if untouchedMods: print("" "Warning: No messages were extracted from the following mods:" "") for mod in untouchedMods: - print( - "• {modName}: {warningMessage}".format( - modName=mod, warningMessage=untouchedMods[mod] - ) - ) + print(f"• {mod}: {untouchedMods[mod]}") print( "" - f"For this script to extract messages from a mod folder, this mod folder must contain a '{l10nFolderName}' " - f"folder, and this folder must contain a '{messagesFilename}' file that describes how to extract messages for the " - f"mod. See the folder of the main mod ('public') for an example, and see the documentation for more " - f"information." + f"For this script to extract messages from a mod folder, this mod folder must contain " + f"a '{l10nFolderName}' folder, and this folder must contain a '{messagesFilename}' " + f"file that describes how to extract messages for the mod. See the folder of the main " + f"mod ('public') for an example, and see the documentation for more information." ) @@ -108,7 +101,7 @@ def generatePOT(templateSettings, rootPath): def generateTemplatesForMessagesFile(messagesFilePath): - with open(messagesFilePath, "r") as fileObject: + with open(messagesFilePath) as fileObject: settings = json.load(fileObject) for templateSettings in settings: @@ -127,7 +120,7 @@ def main(): "Type '.' for current working directory", ) args = parser.parse_args() - for root, folders, filenames in os.walk(args.scandir or projectRootDirectory): + for root, folders, _filenames in os.walk(args.scandir or projectRootDirectory): for folder in folders: if folder == l10nFolderName: messagesFilePath = os.path.join(root, folder, messagesFilename) diff --git a/source/tools/mapcompatibility/a18_to_a19.py b/source/tools/mapcompatibility/a18_to_a19.py index 5ea5a06df8..c94347de52 100755 --- a/source/tools/mapcompatibility/a18_to_a19.py +++ b/source/tools/mapcompatibility/a18_to_a19.py @@ -27,14 +27,17 @@ import os import struct import sys + parser = argparse.ArgumentParser( - description="Convert maps compatible with 0 A.D. version Alpha XVIII (A18) to maps compatible with version Alpha XIX (A19), or the other way around." + description="Convert maps compatible with 0 A.D. version Alpha XVIII (A18) to maps compatible " + "with version Alpha XIX (A19), or the other way around." ) parser.add_argument( "--reverse", action="store_true", - help="Make an A19 map compatible with A18 (note that conversion will fail if mountains are too high)", + help="Make an A19 map compatible with A18 (note that conversion will fail " + "if mountains are too high)", ) parser.add_argument( "--no-version-bump", action="store_true", help="Don't change the version number of the map" @@ -51,7 +54,6 @@ parser.add_argument( ) args = parser.parse_args() - HEIGHTMAP_BIT_SHIFT = 3 for xmlFile in args.files: @@ -68,25 +70,22 @@ for xmlFile in args.files: version = struct.unpack("> HEIGHTMAP_BIT_SHIFT - def height_transform(h): - return h >> HEIGHTMAP_BIT_SHIFT - - for i in range(0, (map_size * 16 + 1) * (map_size * 16 + 1)): + for _i in range((map_size * 16 + 1) * (map_size * 16 + 1)): height = struct.unpack("') == -1: print( - "Warning: File " - + xmlFile - + " was not at version 6, while a negative version bump was requested.\nABORTING ..." + f"Warning: File {xmlFile} was not at version 6, while a negative " + f"version bump was requested.\nABORTING ..." ) sys.exit() else: data = data.replace('', '') + elif data.find('') == -1: + print( + f"Warning: File {xmlFile} was not at version 5, while a version bump " + f"was requested.\nABORTING ..." + ) + sys.exit() else: - if data.find('') == -1: - print( - "Warning: File " - + xmlFile - + " was not at version 5, while a version bump was requested.\nABORTING ..." - ) - sys.exit() - else: - data = data.replace('', '') + data = data.replace('', '') # transform the color keys if not args.no_color_spelling: diff --git a/source/tools/rlclient/python/__init__.py b/source/tools/rlclient/python/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/tools/rlclient/python/samples/__init__.py b/source/tools/rlclient/python/samples/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/tools/rlclient/python/samples/simple-example.py b/source/tools/rlclient/python/samples/simple-example.py index 76d20bfd79..5b00a821e6 100644 --- a/source/tools/rlclient/python/samples/simple-example.py +++ b/source/tools/rlclient/python/samples/simple-example.py @@ -1,17 +1,17 @@ # This script provides an overview of the zero_ad wrapper for 0 AD -from os import path -import zero_ad - # First, we will define some helper functions we will use later. import math +from os import path + +import zero_ad def dist(p1, p2): - return math.sqrt(sum((math.pow(x2 - x1, 2) for (x1, x2) in zip(p1, p2)))) + return math.sqrt(sum(math.pow(x2 - x1, 2) for (x1, x2) in zip(p1, p2))) def center(units): - sum_position = map(sum, zip(*map(lambda u: u.position(), units))) + sum_position = map(sum, zip(*(u.position() for u in units))) return [x / len(units) for x in sum_position] @@ -33,7 +33,7 @@ game = zero_ad.ZeroAD("http://localhost:6000") # Load the Arcadia map samples_dir = path.dirname(path.realpath(__file__)) scenario_config_path = path.join(samples_dir, "arcadia.json") -with open(scenario_config_path, "r") as f: +with open(scenario_config_path, encoding="utf8") as f: arcadia_config = f.read() state = game.reset(arcadia_config) diff --git a/source/tools/rlclient/python/setup.py b/source/tools/rlclient/python/setup.py index 43d4dd82ef..fed25328aa 100644 --- a/source/tools/rlclient/python/setup.py +++ b/source/tools/rlclient/python/setup.py @@ -1,5 +1,6 @@ from setuptools import setup + setup( name="zero_ad", version="0.0.1", diff --git a/source/tools/rlclient/python/tests/__init__.py b/source/tools/rlclient/python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/tools/rlclient/python/tests/test_actions.py b/source/tools/rlclient/python/tests/test_actions.py index c59e513438..deab900d48 100644 --- a/source/tools/rlclient/python/tests/test_actions.py +++ b/source/tools/rlclient/python/tests/test_actions.py @@ -1,19 +1,21 @@ -import zero_ad import math from os import path +import zero_ad + + game = zero_ad.ZeroAD("http://localhost:6000") scriptdir = path.dirname(path.realpath(__file__)) -with open(path.join(scriptdir, "..", "samples", "arcadia.json"), "r") as f: +with open(path.join(scriptdir, "..", "samples", "arcadia.json"), encoding="utf8") as f: config = f.read() def dist(p1, p2): - return math.sqrt(sum((math.pow(x2 - x1, 2) for (x1, x2) in zip(p1, p2)))) + return math.sqrt(sum(math.pow(x2 - x1, 2) for (x1, x2) in zip(p1, p2))) def center(units): - sum_position = map(sum, zip(*map(lambda u: u.position(), units))) + sum_position = map(sum, zip(*(u.position() for u in units))) return [x / len(units) for x in sum_position] diff --git a/source/tools/rlclient/python/tests/test_evaluate.py b/source/tools/rlclient/python/tests/test_evaluate.py index 508bb8c8c7..6ebf8e328a 100644 --- a/source/tools/rlclient/python/tests/test_evaluate.py +++ b/source/tools/rlclient/python/tests/test_evaluate.py @@ -1,12 +1,14 @@ -import zero_ad from os import path +import zero_ad + + game = zero_ad.ZeroAD("http://localhost:6000") scriptdir = path.dirname(path.realpath(__file__)) -with open(path.join(scriptdir, "..", "samples", "arcadia.json"), "r") as f: +with open(path.join(scriptdir, "..", "samples", "arcadia.json")) as f: config = f.read() -with open(path.join(scriptdir, "fastactions.js"), "r") as f: +with open(path.join(scriptdir, "fastactions.js")) as f: fastactions = f.read() diff --git a/source/tools/rlclient/python/zero_ad/__init__.py b/source/tools/rlclient/python/zero_ad/__init__.py index 868f89a842..4baaa9d08a 100644 --- a/source/tools/rlclient/python/zero_ad/__init__.py +++ b/source/tools/rlclient/python/zero_ad/__init__.py @@ -1,5 +1,8 @@ -from . import actions # noqa: F401 -from . import environment +from . import ( + actions, # noqa: F401 + environment, +) + ZeroAD = environment.ZeroAD GameState = environment.GameState diff --git a/source/tools/rlclient/python/zero_ad/api.py b/source/tools/rlclient/python/zero_ad/api.py index 88cc3a2904..f2705a5c61 100644 --- a/source/tools/rlclient/python/zero_ad/api.py +++ b/source/tools/rlclient/python/zero_ad/api.py @@ -1,5 +1,5 @@ -from urllib import request import json +from urllib import request class RLAPI: @@ -11,7 +11,7 @@ class RLAPI: return response.read() def step(self, commands): - post_data = "\n".join((f"{player};{json.dumps(action)}" for (player, action) in commands)) + post_data = "\n".join(f"{player};{json.dumps(action)}" for (player, action) in commands) return self.post("step", post_data) def reset(self, scenario_config, player_id, save_replay): diff --git a/source/tools/rlclient/python/zero_ad/environment.py b/source/tools/rlclient/python/zero_ad/environment.py index bb3e2fcbde..4acb00e2c8 100644 --- a/source/tools/rlclient/python/zero_ad/environment.py +++ b/source/tools/rlclient/python/zero_ad/environment.py @@ -1,7 +1,8 @@ -from .api import RLAPI import json -from xml.etree import ElementTree from itertools import cycle +from xml.etree import ElementTree as ET + +from .api import RLAPI class ZeroAD: @@ -11,7 +12,9 @@ class ZeroAD: self.cache = {} self.player_id = 1 - def step(self, actions=[], player=None): + def step(self, actions=None, player=None): + if actions is None: + actions = [] player_ids = cycle([self.player_id]) if player is None else cycle(player) cmds = zip(player_ids, actions) @@ -35,8 +38,10 @@ class ZeroAD: templates = self.api.get_templates(names) return [(name, EntityTemplate(content)) for (name, content) in templates] - def update_templates(self, types=[]): - all_types = list(set([unit.type() for unit in self.current_state.units()])) + def update_templates(self, types=None): + if types is None: + types = [] + all_types = list({unit.type() for unit in self.current_state.units()}) all_types += types template_pairs = self.get_templates(all_types) @@ -106,7 +111,7 @@ class Entity: class EntityTemplate: def __init__(self, xml): - self.data = ElementTree.fromstring(f"{xml}") + self.data = ET.fromstring(f"{xml}") def get(self, path): node = self.data.find(path) @@ -120,4 +125,4 @@ class EntityTemplate: return node is not None def __str__(self): - return ElementTree.tostring(self.data).decode("utf-8") + return ET.tostring(self.data).decode("utf-8") diff --git a/source/tools/spirv/compile.py b/source/tools/spirv/compile.py old mode 100644 new mode 100755 index 31d2078c1a..b14fad0cbc --- a/source/tools/spirv/compile.py +++ b/source/tools/spirv/compile.py @@ -28,10 +28,10 @@ import json import os import subprocess import sys -import yaml - import xml.etree.ElementTree as ET +import yaml + def execute(command): try: @@ -81,9 +81,8 @@ def resolve_if(defines, expression): if define["value"] != "1": return True found_define = True - else: - if define["value"] == "1": - return True + elif define["value"] == "1": + return True if invert and not found_define: return True return False @@ -124,12 +123,11 @@ def compile_and_reflect( command.append("-DSTAGE_{}={}".format(stage.upper(), "1")) command += ["-o", output_path] # Compile the shader with debug information to see names in reflection. - ret, out, err = execute(command + ["-g"]) + ret, out, err = execute([*command, "-g"]) if ret: sys.stderr.write( - "Command returned {}:\nCommand: {}\nInput path: {}\nOutput path: {}\nError: {}\n".format( - ret, " ".join(command), input_path, output_path, err - ) + "Command returned {}:\nCommand: {}\nInput path: {}\nOutput path: {}\n" + "Error: {}\n".format(ret, " ".join(command), input_path, output_path, err) ) preprocessor_output_path = os.path.abspath( os.path.join(os.path.dirname(__file__), "preprocessed_file.glsl") @@ -139,24 +137,23 @@ def compile_and_reflect( ret, out, err = execute(["spirv-reflect", "-y", "-v", "1", output_path]) if ret: sys.stderr.write( - "Command returned {}:\nCommand: {}\nInput path: {}\nOutput path: {}\nError: {}\n".format( - ret, " ".join(command), input_path, output_path, err - ) + "Command returned {}:\nCommand: {}\nInput path: {}\nOutput path: {}\n" + "Error: {}\n".format(ret, " ".join(command), input_path, output_path, err) ) raise ValueError(err) # Reflect the result SPIRV. data = yaml.safe_load(out) module = data["module"] interface_variables = [] - if "all_interface_variables" in data and data["all_interface_variables"]: + if data.get("all_interface_variables"): interface_variables = data["all_interface_variables"] push_constants = [] vertex_attributes = [] - if "push_constants" in module and module["push_constants"]: + if module.get("push_constants"): assert len(module["push_constants"]) == 1 def add_push_constants(node, push_constants): - if ("members" in node) and node["members"]: + if node.get("members"): for member in node["members"]: add_push_constants(member, push_constants) else: @@ -173,7 +170,7 @@ def compile_and_reflect( assert module["push_constants"][0]["size"] <= 128 add_push_constants(module["push_constants"][0], push_constants) descriptor_sets = [] - if "descriptor_sets" in module and module["descriptor_sets"]: + if module.get("descriptor_sets"): VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER = 1 VK_DESCRIPTOR_TYPE_STORAGE_IMAGE = 3 VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER = 6 @@ -232,43 +229,39 @@ def compile_and_reflect( "name": binding["name"], } ) - else: - if use_descriptor_indexing: - if descriptor_set["set"] == 0: - assert descriptor_set["binding_count"] >= 1 - for binding in descriptor_set["bindings"]: - assert ( - binding["descriptor_type"] - == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER - ) - assert binding["array"]["dims"][0] == 16384 - if binding["binding"] == 0: - assert binding["name"] == "textures2D" - elif binding["binding"] == 1: - assert binding["name"] == "texturesCube" - elif binding["binding"] == 2: - assert binding["name"] == "texturesShadow" - else: - assert False - else: - assert descriptor_set["binding_count"] > 0 + elif use_descriptor_indexing: + if descriptor_set["set"] == 0: + assert descriptor_set["binding_count"] >= 1 for binding in descriptor_set["bindings"]: assert ( binding["descriptor_type"] == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER ) - assert binding["image"]["sampled"] == 1 - assert binding["image"]["arrayed"] == 0 - assert binding["image"]["ms"] == 0 - sampler_type = "sampler{}D".format(binding["image"]["dim"] + 1) - if binding["image"]["dim"] == 3: - sampler_type = "samplerCube" - bindings.append( - { - "binding": binding["binding"], - "type": sampler_type, - "name": binding["name"], - } - ) + assert binding["array"]["dims"][0] == 16384 + if binding["binding"] == 0: + assert binding["name"] == "textures2D" + elif binding["binding"] == 1: + assert binding["name"] == "texturesCube" + elif binding["binding"] == 2: + assert binding["name"] == "texturesShadow" + else: + raise AssertionError + else: + assert descriptor_set["binding_count"] > 0 + for binding in descriptor_set["bindings"]: + assert binding["descriptor_type"] == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER + assert binding["image"]["sampled"] == 1 + assert binding["image"]["arrayed"] == 0 + assert binding["image"]["ms"] == 0 + sampler_type = "sampler{}D".format(binding["image"]["dim"] + 1) + if binding["image"]["dim"] == 3: + sampler_type = "samplerCube" + bindings.append( + { + "binding": binding["binding"], + "type": sampler_type, + "name": binding["name"], + } + ) descriptor_sets.append( { "set": descriptor_set["set"], @@ -290,9 +283,8 @@ def compile_and_reflect( ret, out, err = execute(command) if ret: sys.stderr.write( - "Command returned {}:\nCommand: {}\nInput path: {}\nOutput path: {}\nError: {}\n".format( - ret, " ".join(command), input_path, output_path, err - ) + "Command returned {}:\nCommand: {}\nInput path: {}\nOutput path: {}\n" + "Error: {}\n".format(ret, " ".join(command), input_path, output_path, err) ) raise ValueError(err) return { @@ -304,30 +296,28 @@ def compile_and_reflect( def output_xml_tree(tree, path): """We use a simple custom printer to have the same output for all platforms.""" - with open(path, "wt") as handle: + with open(path, "w") as handle: handle.write('\n') - handle.write( - "\n".format(os.path.basename(__file__)) - ) + handle.write(f"\n") def output_xml_node(node, handle, depth): indent = "\t" * depth attributes = "" for attribute_name in sorted(node.attrib.keys()): - attributes += ' {}="{}"'.format(attribute_name, node.attrib[attribute_name]) + attributes += f' {attribute_name}="{node.attrib[attribute_name]}"' if len(node) > 0: - handle.write("{}<{}{}>\n".format(indent, node.tag, attributes)) + handle.write(f"{indent}<{node.tag}{attributes}>\n") for child in node: output_xml_node(child, handle, depth + 1) - handle.write("{}\n".format(indent, node.tag)) + handle.write(f"{indent}\n") else: - handle.write("{}<{}{}/>\n".format(indent, node.tag, attributes)) + handle.write(f"{indent}<{node.tag}{attributes}/>\n") output_xml_node(tree.getroot(), handle, 0) def build(rules, input_mod_path, output_mod_path, dependencies, program_name): - sys.stdout.write('Program "{}"\n'.format(program_name)) + sys.stdout.write(f'Program "{program_name}"\n') if rules and program_name not in rules: sys.stdout.write(" Skip.\n") return @@ -392,7 +382,7 @@ def build(rules, input_mod_path, output_mod_path, dependencies, program_name): } ) else: - raise ValueError('Unsupported element tag: "{}"'.format(element_tag)) + raise ValueError(f'Unsupported element tag: "{element_tag}"') stage_extension = { "vertex": ".vs", @@ -525,9 +515,9 @@ def build(rules, input_mod_path, output_mod_path, dependencies, program_name): member_element.set("name", member["name"]) member_element.set("size", member["size"]) member_element.set("offset", member["offset"]) - elif binding["type"].startswith("sampler"): - binding_element.set("name", binding["name"]) - elif binding["type"].startswith("storage"): + elif binding["type"].startswith("sampler") or binding["type"].startswith( + "storage" + ): binding_element.set("name", binding["name"]) program_tree = ET.ElementTree(program_root) output_xml_tree(program_tree, os.path.join(output_mod_path, "shaders", program_path)) @@ -540,18 +530,21 @@ def run(): parser = argparse.ArgumentParser() parser.add_argument( "input_mod_path", - help="a path to a directory with input mod with GLSL shaders like binaries/data/mods/public", + help="a path to a directory with input mod with GLSL shaders " + "like binaries/data/mods/public", ) parser.add_argument("rules_path", help="a path to JSON with rules") parser.add_argument( "output_mod_path", - help="a path to a directory with mod to store SPIR-V shaders like binaries/data/mods/spirv", + help="a path to a directory with mod to store SPIR-V shaders " + "like binaries/data/mods/spirv", ) parser.add_argument( "-d", "--dependency", action="append", - help="a path to a directory with a dependency mod (at least modmod should present as dependency)", + help="a path to a directory with a dependency mod (at least " + "modmod should present as dependency)", required=True, ) parser.add_argument( @@ -563,26 +556,26 @@ def run(): args = parser.parse_args() if not os.path.isfile(args.rules_path): - sys.stderr.write('Rules "{}" are not found\n'.format(args.rules_path)) + sys.stderr.write(f'Rules "{args.rules_path}" are not found\n') return - with open(args.rules_path, "rt") as handle: + with open(args.rules_path) as handle: rules = json.load(handle) if not os.path.isdir(args.input_mod_path): - sys.stderr.write('Input mod path "{}" is not a directory\n'.format(args.input_mod_path)) + sys.stderr.write(f'Input mod path "{args.input_mod_path}" is not a directory\n') return if not os.path.isdir(args.output_mod_path): - sys.stderr.write('Output mod path "{}" is not a directory\n'.format(args.output_mod_path)) + sys.stderr.write(f'Output mod path "{args.output_mod_path}" is not a directory\n') return mod_shaders_path = os.path.join(args.input_mod_path, "shaders", "glsl") if not os.path.isdir(mod_shaders_path): - sys.stderr.write('Directory "{}" was not found\n'.format(mod_shaders_path)) + sys.stderr.write(f'Directory "{mod_shaders_path}" was not found\n') return mod_name = os.path.basename(os.path.normpath(args.input_mod_path)) - sys.stdout.write('Building SPIRV for "{}"\n'.format(mod_name)) + sys.stdout.write(f'Building SPIRV for "{mod_name}"\n') if not args.program_name: for file_name in os.listdir(mod_shaders_path): name, ext = os.path.splitext(file_name) diff --git a/source/tools/templatesanalyzer/unitTables.py b/source/tools/templatesanalyzer/unitTables.py old mode 100644 new mode 100755 index 3606e36d3b..e541d0fd38 --- a/source/tools/templatesanalyzer/unitTables.py +++ b/source/tools/templatesanalyzer/unitTables.py @@ -21,15 +21,15 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import glob +import os import sys - import xml.etree.ElementTree as ET from pathlib import Path -import os -import glob + sys.path.append("../entity") -from scriptlib import SimulTemplateEntity # noqa: E402 +from scriptlib import SimulTemplateEntity AttackTypes = ["Hack", "Pierce", "Crush", "Poison", "Fire"] @@ -299,15 +299,15 @@ def CalcUnit(UnitName, existingUnit=None): def WriteUnit(Name, UnitDict): ret = "" ret += '' + Name + "" - ret += "" + str("%.0f" % float(UnitDict["HP"])) + "" - ret += "" + str("%.0f" % float(UnitDict["BuildTime"])) + "" - ret += "" + str("%.1f" % float(UnitDict["WalkSpeed"])) + "" + ret += "" + str("{:.0f}".format(float(UnitDict["HP"]))) + "" + ret += "" + str("{:.0f}".format(float(UnitDict["BuildTime"]))) + "" + ret += "" + str("{:.1f}".format(float(UnitDict["WalkSpeed"]))) + "" for atype in AttackTypes: PercentValue = 1.0 - (0.9 ** float(UnitDict["Resistance"][atype])) ret += ( "" - + str("%.0f" % float(UnitDict["Resistance"][atype])) + + str("{:.0f}".format(float(UnitDict["Resistance"][atype]))) + " / " + str("%.0f" % (PercentValue * 100.0)) + "%" @@ -325,28 +325,28 @@ def WriteUnit(Name, UnitDict): ret += "" + str("%.1f" % (float(UnitDict["RepeatRate"][attType]) / 1000.0)) + "" else: - for atype in AttackTypes: + for _ in AttackTypes: ret += " - " ret += " - " if UnitDict["Ranged"] is True and UnitDict["Range"] > 0: - ret += "" + str("%.1f" % float(UnitDict["Range"])) + "" + ret += "" + str("{:.1f}".format(float(UnitDict["Range"]))) + "" spread = float(UnitDict["Spread"]) - ret += "" + str("%.1f" % spread) + "" + ret += "" + str(f"{spread:.1f}") + "" else: ret += " - - " for rtype in Resources: - ret += "" + str("%.0f" % float(UnitDict["Cost"][rtype])) + "" + ret += "" + str("{:.0f}".format(float(UnitDict["Cost"][rtype]))) + "" - ret += "" + str("%.0f" % float(UnitDict["Cost"]["population"])) + "" + ret += "" + str("{:.0f}".format(float(UnitDict["Cost"]["population"]))) + "" ret += '' for Bonus in UnitDict["AttackBonuses"]: ret += "[" for classe in UnitDict["AttackBonuses"][Bonus]["Classes"]: ret += classe + " " - ret += ": %s] " % UnitDict["AttackBonuses"][Bonus]["Multiplier"] + ret += ": {}] ".format(UnitDict["AttackBonuses"][Bonus]["Multiplier"]) ret += "" ret += "\n" @@ -370,7 +370,7 @@ def SortFn(A): def WriteColouredDiff(file, diff, isChanged): - """helper to write coloured text. + """Helper to write coloured text. diff value must always be computed as a unit_spec - unit_generic. A positive imaginary part represents advantageous trait. """ @@ -378,8 +378,7 @@ def WriteColouredDiff(file, diff, isChanged): def cleverParse(diff): if float(diff) - int(diff) < 0.001: return str(int(diff)) - else: - return str("%.1f" % float(diff)) + return str(f"{float(diff):.1f}") isAdvantageous = diff.imag > 0 diff = diff.real @@ -392,16 +391,14 @@ def WriteColouredDiff(file, diff, isChanged): if diff == 0: rgb_str = "200,200,200" - elif isAdvantageous and diff > 0: - rgb_str = "180,0,0" - elif (not isAdvantageous) and diff < 0: + elif isAdvantageous and diff > 0 or (not isAdvantageous) and diff < 0: rgb_str = "180,0,0" else: rgb_str = "0,150,0" file.write( - """{} - """.format(rgb_str, cleverParse(diff)) + f"""{cleverParse(diff)} + """ ) return isChanged @@ -743,7 +740,8 @@ differences between the two. isChanged = WriteColouredDiff(ff, +1j + (mySpread - parentSpread), isChanged) else: ff.write( - "--" + "-" + "-" ) else: ff.write("") @@ -769,9 +767,7 @@ differences between the two. ff.write("\n") ff.close() # to actually write into the file - with open( - os.path.realpath(__file__).replace("unitTables.py", "") + ".cache", "r" - ) as ff: + with open(os.path.realpath(__file__).replace("unitTables.py", "") + ".cache") as ff: unitStr = ff.read() if showChangedOnly: @@ -832,7 +828,7 @@ each loaded generic template. ) for civ in Civs: count = 0 - for units in CivTemplates[civ]: + for _units in CivTemplates[civ]: count += 1 f.write('' + str(count) + "\n") diff --git a/source/tools/xmlvalidator/validate_grammar.py b/source/tools/xmlvalidator/validate_grammar.py old mode 100644 new mode 100755 index 6b830fb883..a692237687 --- a/source/tools/xmlvalidator/validate_grammar.py +++ b/source/tools/xmlvalidator/validate_grammar.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 -from argparse import ArgumentParser -from pathlib import Path -from os.path import join, realpath, exists, dirname -from json import load -from re import match -from logging import getLogger, StreamHandler, INFO, WARNING, Filter, Formatter -import lxml.etree import sys +from argparse import ArgumentParser +from json import load +from logging import INFO, WARNING, Filter, Formatter, StreamHandler, getLogger +from os.path import dirname, exists, join, realpath +from pathlib import Path +from re import match + +import lxml.etree class SingleLevelFilter(Filter): @@ -17,8 +18,7 @@ class SingleLevelFilter(Filter): def filter(self, record): if self.reject: return record.levelno != self.passlevel - else: - return record.levelno == self.passlevel + return record.levelno == self.passlevel class VFS_File: @@ -68,8 +68,8 @@ class RelaxNGValidator: def main(self): """Program entry point, parses command line arguments and launches the validation""" # ordered uniq mods (dict maintains ordered keys from python 3.6) - self.logger.info(f"Checking {'|'.join(self.mods)}'s integrity.") - self.logger.info(f"The following mods will be loaded: {'|'.join(self.mods)}.") + self.logger.info("Checking %s's integrity.", "|".join(self.mods)) + self.logger.info("The following mods will be loaded: %s.", "|".join(self.mods)) return self.run() def find_files(self, vfs_root, mods, vfs_path, *ext_list): @@ -83,7 +83,7 @@ class RelaxNGValidator: def find_recursive(dp, base): """(relative Path, full Path) generator""" if dp.is_dir(): - if dp.name != ".svn" and dp.name != ".git" and not dp.name.endswith("~"): + if dp.name not in (".svn", ".git") and not dp.name.endswith("~"): for fp in dp.iterdir(): yield from find_recursive(fp, base) elif dp.suffix in full_exts: @@ -191,7 +191,7 @@ class RelaxNGValidator: def validate_files(self, name, files, schemapath): relax_ng_path = self.get_relaxng_file(schemapath) if relax_ng_path == "": - self.logger.warning(f"Could not find {schemapath}") + self.logger.warning("Could not find %s", schemapath) return data = lxml.etree.parse(relax_ng_path) @@ -201,14 +201,14 @@ class RelaxNGValidator: try: doc = lxml.etree.parse(str(file[1])) relaxng.assertValid(doc) - except Exception as e: + except Exception: error_count = error_count + 1 - self.logger.error(f"{file[1]}: " + str(e)) + self.logger.exception(file[1]) if self.verbose: - self.logger.info(f"{error_count} {name} validation errors") + self.logger.info("%d %s validation errors", error_count, name) elif error_count > 0: - self.logger.error(f"{error_count} {name} validation errors") + self.logger.error("%d %s validation errors", error_count, name) self.inError = True diff --git a/source/tools/xmlvalidator/validator.py b/source/tools/xmlvalidator/validator.py old mode 100644 new mode 100755 index cd354fac48..9a59a637b7 --- a/source/tools/xmlvalidator/validator.py +++ b/source/tools/xmlvalidator/validator.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 import argparse import os -import sys import re -import xml.etree.ElementTree -from logging import getLogger, StreamHandler, INFO, WARNING, Formatter, Filter +import sys +from logging import INFO, WARNING, Filter, Formatter, StreamHandler, getLogger +from xml.etree import ElementTree as ET class SingleLevelFilter(Filter): @@ -15,8 +15,7 @@ class SingleLevelFilter(Filter): def filter(self, record): if self.reject: return record.levelno != self.passlevel - else: - return record.levelno == self.passlevel + return record.levelno == self.passlevel class Actor: @@ -30,9 +29,9 @@ class Actor: def read(self, physical_path): try: - tree = xml.etree.ElementTree.parse(physical_path) - except xml.etree.ElementTree.ParseError as err: - self.logger.error('"%s": %s' % (physical_path, err.msg)) + tree = ET.parse(physical_path) + except ET.ParseError: + self.logger.exception(physical_path) return False root = tree.getroot() # Special case: particles don't need a diffuse texture. @@ -52,10 +51,10 @@ class Actor: def read_variant(self, actor_physical_path, relative_path): physical_path = actor_physical_path.replace(self.vfs_path, relative_path) try: - tree = xml.etree.ElementTree.parse(physical_path) - except xml.etree.ElementTree.ParseError as err: - self.logger.error('"%s": %s' % (physical_path, err.msg)) - return False + tree = ET.parse(physical_path) + except ET.ParseError: + self.logger.exception(physical_path) + return root = tree.getroot() file = root.get("file") @@ -75,9 +74,9 @@ class Material: def read(self, physical_path): try: - root = xml.etree.ElementTree.parse(physical_path).getroot() - except xml.etree.ElementTree.ParseError as err: - self.logger.error('"%s": %s' % (physical_path, err.msg)) + root = ET.parse(physical_path).getroot() + except ET.ParseError: + self.logger.exception(physical_path) return False for element in root.findall(".//required_texture"): texture_name = element.get("name") @@ -127,7 +126,7 @@ class Validator: if not os.path.isdir(physical_path): return result for file_name in os.listdir(physical_path): - if file_name == ".git" or file_name == ".svn": + if file_name in (".git", ".svn"): continue vfs_file_path = os.path.join(vfs_path, file_name) physical_file_path = os.path.join(physical_path, file_name) @@ -180,8 +179,9 @@ class Validator: and actor.material not in self.invalid_materials ): self.logger.error( - '"%s": unknown material "%s"' - % (self.get_mod_path(actor.mod_name, actor.vfs_path), actor.material) + '"%s": unknown material "%s"', + self.get_mod_path(actor.mod_name, actor.vfs_path), + actor.material, ) self.inError = True if actor.material not in self.materials: @@ -189,42 +189,34 @@ class Validator: material = self.materials[actor.material] missing_textures = ", ".join( - set( - [ - required_texture - for required_texture in material.required_textures - if required_texture not in actor.textures - ] - ) + { + required_texture + for required_texture in material.required_textures + if required_texture not in actor.textures + } ) if len(missing_textures) > 0: self.logger.error( - '"%s": actor does not contain required texture(s) "%s" from "%s"' - % ( - self.get_mod_path(actor.mod_name, actor.vfs_path), - missing_textures, - material.name, - ) + '"%s": actor does not contain required texture(s) "%s" from "%s"', + self.get_mod_path(actor.mod_name, actor.vfs_path), + missing_textures, + material.name, ) self.inError = True extra_textures = ", ".join( - set( - [ - extra_texture - for extra_texture in actor.textures - if extra_texture not in material.required_textures - ] - ) + { + extra_texture + for extra_texture in actor.textures + if extra_texture not in material.required_textures + } ) if len(extra_textures) > 0: self.logger.warning( - '"%s": actor contains unnecessary texture(s) "%s" from "%s"' - % ( - self.get_mod_path(actor.mod_name, actor.vfs_path), - extra_textures, - material.name, - ) + '"%s": actor contains unnecessary texture(s) "%s" from "%s"', + self.get_mod_path(actor.mod_name, actor.vfs_path), + extra_textures, + material.name, ) self.inError = True