ldcheck.py

Thu, 26 Aug 2021 22:04:33 +0300

author
Teemu Piippo <teemu@hecknology.net>
date
Thu, 26 Aug 2021 22:04:33 +0300
changeset 152
5e347a96869a
parent 150
fcc07f6907a8
permissions
-rwxr-xr-x

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)

mercurial