Thu, 26 Aug 2021 22:04:33 +0300
removed some alias tests that aren't mandated by the official header specification
#!/usr/bin/env python3 import sys if sys.version_info < (3, 4): raise RuntimeError('Python 3.4 or newer required') from colours import load_colours try: import colorama except ImportError: colorama = None appname = 'ldcheck' version = (1, 0, 9999) version_string = str.join('.', map(str, version)) from geometry import * from pathlib import Path import linetypes import header import parse from os.path import realpath script_directory = Path(realpath(__file__)).parent def check_library_paths(library_paths): for library_path in library_paths: if not library_path.exists(): raise RuntimeError(str.format( 'error: library path {} does not exist', library_path, )) elif not library_path.exists(): raise RuntimeError(str.format( 'error: library path {} is not a directory', library_path, )) def find_ldconfig_ldr_paths(libraries): for library_path in libraries: yield from [ library_path / path for path in ['LDConfig.ldr', 'ldconfig.ldr'] if (library_path / path).is_file() ] def load_ldconfig(libraries): ldconfig_ldr_paths = list(find_ldconfig_ldr_paths(libraries)) if not ldconfig_ldr_paths: raise RuntimeError('could not find any LDConfig.ldr') for ldconfig_ldr_path in ldconfig_ldr_paths: with ldconfig_ldr_path.open() as ldconfig_ldr: load_colours(ldconfig_ldr) class LDrawContext: ''' Contains context-dependant LDraw information, like library directory paths and the colour table. ''' def __init__(self, libraries): self._libraries = libraries self.ldconfig_colour_data = self.load_ldconfig_ldr() self.check_library_paths() @property def libraries(self): return self._libraries @property def colours(self): return self.ldconfig_colour_data def ldconfig_ldr_discovery(self): for library_path in self.libraries: yield from [ library_path / path for path in ['LDConfig.ldr', 'ldconfig.ldr'] if (library_path / path).is_file() ] def load_ldconfig_ldr(self): from colours import load_colours for ldconfig_ldr_path in self.ldconfig_ldr_discovery(): with open(ldconfig_ldr_path) as ldconfig_ldr: return load_colours(ldconfig_ldr) def check_library_paths(self): from sys import stderr problems = False have_paths = False for library_path in self.libraries: have_paths = True if not library_path.exists(): problems = True print(str.format( 'Library path {} does not exist', library_path, ), file = stderr) elif not library_path.exists(): problems = True print(str.format( 'Library path {} is not a directory', library_path, ), file = stderr) if not have_paths: raise RuntimeError('no LDraw library paths specified') def is_ldconfig_colour(self, colour): return colour.index in self.colours def colour_name(self, colour): if self.is_ldconfig_colour(colour): return self.colours[self.index]['name'] else: return str(colour) def colour_face(self, colour): if self.is_ldconfig_colour(colour): return self.colours[self.index]['value'] elif colour.is_direct_colour: return '#%06X' % (self.index & 0xffffff) else: return '#000000' def is_valid_colour(self, colour): return self.is_ldconfig_colour(colour) or colour.is_direct_colour def load_rcfile(): import os rcpath = Path(os.path.expanduser('~/.config/ldcheckrc')) if rcpath.exists(): with rcpath.open() as file: return ['--' + line.strip() for line in file] else: return [] def parse_commandline_arguments(): import argparse class ListProblemTypesAction(argparse.Action): def __init__(self, option_strings, dest, nargs = None, **kwargs): super().__init__(option_strings, dest, nargs = 0, **kwargs) def __call__(self, *args, **kwargs): import testsuite test_suite = testsuite.load_tests() for warning_type in testsuite.all_problem_types(test_suite): print(str.format('{name}: {severity}: "{message}"', name = warning_type.name, severity = warning_type.severity, message = warning_type.placeholder_message(), )) sys.exit(0) parser = argparse.ArgumentParser() parser.add_argument('filename') parser.add_argument('-l', '--library', action = 'append') parser.add_argument('--list', action = ListProblemTypesAction, help = 'lists all possible problem types and exit', ) parser.add_argument('--dump', action = 'store_true', help = 'dumps the internal parsed structure of the part file', ) parser.add_argument('--rebuild', action = 'store_true', help = 'parses the part file and prints it back out, used for ' 'testing whether the program interprets part files correctly', ) parser.add_argument('--subfile', action = 'store_true', help = 'finds a subfile by name and prints out information about it' ) parser.add_argument('--color', action = 'store_true', help = 'use colors' ) parser.add_argument('-d', '--ldraw-dir', nargs = '+', help = 'specify LDraw directory path(s)', ) parser.add_argument('-v', '--version', action = 'version', version = str.format('{appname} {version}', appname = appname, version = version_string, ), ) return parser.parse_args(load_rcfile() + sys.argv[1:]) def format_report(report, model, test_suite, *, use_colors = True): from testsuite import problem_text messages = [] for problem in report['problems']: text_colour = '' if use_colors: if problem.severity == 'hold': text_colour = colorama.Fore.LIGHTRED_EX elif problem.severity == 'warning': text_colour = colorama.Fore.LIGHTBLUE_EX ldraw_code = model.body[problem.body_index].textual_representation() message = str.format( '{text_colour}{model_name}:{line_number}: {problem_type}: {message}' '{colour_reset}\n\t{ldraw_code}', text_colour = text_colour, model_name = model.name, line_number = problem.line_number, problem_type = problem.severity, message = problem_text(problem, test_suite), colour_reset = use_colors and colorama.Fore.RESET or '', ldraw_code = ldraw_code, ) messages.append(message) return '\n'.join(messages) def postprocess_library_paths(libraries_strings): import os return [Path(os.path.expanduser(library)) for library in libraries_strings] def main(): args = parse_commandline_arguments() # Make sure that we have at least one library path specified. if not args.library: raise RuntimeError( 'Please specify libraries using the -l / --library switch.\n' 'For example: -l ~/ldraw or --library=~/ldraw\n' 'Multiple --library switches may be used.') # Prepare the list of libraries. This also expands the ~ for the home # directory import os libraries = postprocess_library_paths(args.library) check_library_paths(libraries) load_ldconfig(libraries) try: context = LDrawContext(libraries) except RuntimeError as error: print('error:', str(error), file = stderr) exit(1) if args.color: try: import colorama colorama.init() except ImportError: print('Use of --color requires the colorama module, disabling colours', file = stderr) args.color = False if args.subfile: # Subfile debug mode: searches for the specified subfile from the LDraw # libraries, opens it as if it was referenced by something and prints # out all information that is calculated from this subfile. import filecache cache = filecache.SubfileCache(context = context) subfile = cache.prepare_file(args.filename) if not subfile.valid: print(subfile.problem) else: print('Flat dimensions:', repr(subfile.flatness)) print('Description:', repr(subfile.description)) print('Contains studs:', repr(subfile.has_studs)) else: with open(args.filename, 'rb') as file: try: from os.path import basename model = parse.read_ldraw( file, name = basename(args.filename), context = context) if args.dump: # Dump mode: prints out the structure of the processed LDraw file print('header: ' + type(model.header).__name__) for key in sorted(dir(model.header)): if not key.startswith('__'): print('\t' + key + ': ' + repr(getattr(model.header, key))) for i, entry in enumerate(model.body): if model.header.valid and i == model.header_size: # Mark where the header is considered to end print('--------- End of header') print(entry) elif args.rebuild: # Debug rebuild mode: open the file, parse it and turn it back # into LDraw code and write it into stdout. This is used to ensure # that LDCheck does not miss any information while parsing files. for entry in model.body: print(entry.textual_representation(), end = '\r\n') else: # Regular mode from testsuite import load_tests, check_model # load the test suite # TODO: maybe add some command line argument to filter tests # in case the user wants to run some specific tests only or # possibly leave some test out test_suite = load_tests() # use the test suite to check the model report = check_model(model, test_suite) # print out the report print(format_report( report, model, test_suite, use_colors = args.color )) except FileNotFoundError: print(str.format( 'no such file: {filename!r}', filename = args.filename ), file = stderr) exit(1) if __name__ == '__main__': try: main() except RuntimeError as e: import sys print('error:', str(e), file = sys.stderr) sys.exit(1)