refactoring: moved context-dependant data to new class LDrawContext. ldcheck no longer writes the config file, and looks for it in sensible locations like ~/.config and /etc. LDraw libraries can now be specified on the command line.

Fri, 18 Sep 2020 23:51:45 +0300

author
Teemu Piippo <teemu@hecknology.net>
date
Fri, 18 Sep 2020 23:51:45 +0300
changeset 145
fde18c4d6784
parent 144
eb4c767522ac
child 147
bec55b021ae7

refactoring: moved context-dependant data to new class LDrawContext. ldcheck no longer writes the config file, and looks for it in sensible locations like ~/.config and /etc. LDraw libraries can now be specified on the command line.

colours.py file | annotate | diff | comparison | revisions
filecache.py file | annotate | diff | comparison | revisions
ldcheck.py file | annotate | diff | comparison | revisions
parse.py file | annotate | diff | comparison | revisions
tests/misc.py file | annotate | diff | comparison | revisions
tests/subfiles.py file | annotate | diff | comparison | revisions
unittest.py file | annotate | diff | comparison | revisions
webfront.py file | annotate | diff | comparison | revisions
--- a/colours.py	Fri Sep 18 21:57:36 2020 +0300
+++ b/colours.py	Fri Sep 18 23:51:45 2020 +0300
@@ -15,33 +15,10 @@
         else:
             return str(self.index)
     def __repr__(self):
-        try:
-            return 'colours.' + colours_inverse_dict[self.index]
-        except KeyError:
-            return str.format('Colour({!r})', self.index)
+        return str.format('Colour({!r})', self.index)
     @property
     def is_direct_colour(self):
         return self.index >= 0x2000000
-    @property
-    def is_ldconfig_colour(self):
-        return self.index in ldconfig_colour_data
-    @property
-    def name(self):
-        if self.is_ldconfig_colour:
-            return ldconfig_colour_data[self.index]['name']
-        else:
-            return str(self)
-    @property
-    def face_colour(self):
-        if self.is_ldconfig_colour:
-            return ldconfig_colour_data[self.index]['value']
-        elif self.is_direct_colour:
-            return '#%06X' % (self.index & 0xffffff)
-        else:
-            return '#000000'
-    @property
-    def is_valid(self):
-        return self.is_ldconfig_colour or self.is_direct_colour
     def __eq__(self, other):
         return self.index == other.index
     def __lt__(self, other):
@@ -98,27 +75,16 @@
             colour = parse_ldconfig_ldr_line(line)
             yield (colour['code'], colour)
 
-class colours:
-    '''
-        LDConfig colour namespace, exists for interactive mode and for
-        Colour.__repr__ to return something pretty.
-    '''
-    pass
-
-# LDConfig lookup tables
-colours_inverse_dict = {}
-ldconfig_colour_data = {}
 
 def load_colours(ldconfig_ldr):
     '''
         Loads colours. Expects a file pointer to LDConfig.ldr as the parameter.
+        Returns a lookup table
     '''
-    global ldconfig_colour_data
     ldconfig_colour_data = dict(parse_ldconfig_ldr(ldconfig_ldr))
     for index, colour in ldconfig_colour_data.items():
         identifier = colour['name'].replace(' ', '_').lower()
-        setattr(colours, identifier, Colour(index))
-        colours_inverse_dict[index] = identifier
+    return ldconfig_colour_data
 
 # Interactive mode support (pass LDConfig.ldr path as a command-line argument)
 if __name__ == '__main__':
--- a/filecache.py	Fri Sep 18 21:57:36 2020 +0300
+++ b/filecache.py	Fri Sep 18 23:51:45 2020 +0300
@@ -40,19 +40,12 @@
             self.problem = None
             self.has_studs = None # Whether or not it contains studs
             self.vertices = set()
-    def __init__(self, ldraw_directories):
+    def __init__(self, context):
         '''
             Initializes a new subfile cache
         '''
         self.cache = dict()
-        if ldraw_directories and isinstance(ldraw_directories[0], str):
-            self.ldraw_directories = [
-                Path(os.path.expanduser(directory))
-                for directory in ldraw_directories
-            ]
-        else:
-            from copy import copy
-            self.ldraw_directories = copy(ldraw_directories)
+        self.context = context
         self.reference_stack = []
     def flatness_of(self, filename):
         '''
@@ -69,7 +62,7 @@
     def find_file(self, filename):
         return find_ldraw_file(
             filename = filename,
-            libraries = self.ldraw_directories,
+            libraries = self.context.libraries,
         )
     def prepare_file(self, filename):
         '''
@@ -90,10 +83,7 @@
         try:
             path = self.find_file(filename)
             with path.open('rb') as file:
-                model = parse.read_ldraw(
-                    file,
-                    ldraw_directories = self.ldraw_directories,
-                )
+                model = parse.read_ldraw(file, context = self.context)
         except (FileNotFoundError, IOError, PermissionError) as error:
             subfile.valid = False
             subfile.problem = str(error)
@@ -129,6 +119,3 @@
                     for subfile_reference in model.subfile_references
                 )
         self.reference_stack.pop()
-
-if __name__ == '__main__':
-    cache = SubfileCache(ldraw_directories = ["~/ldraw"])
--- a/ldcheck.py	Fri Sep 18 21:57:36 2020 +0300
+++ b/ldcheck.py	Fri Sep 18 23:51:45 2020 +0300
@@ -1,4 +1,5 @@
 #!/usr/bin/env python3
+import argparse
 from sys import version_info
 if version_info < (3, 4):
     raise RuntimeError('Python 3.4 or newer required')
@@ -7,7 +8,6 @@
 version = (1, 0, 9999)
 version_string = str.join('.', map(str, version))
 
-from colours import load_colours
 from geometry import *
 from pathlib import Path
 import linetypes
@@ -17,59 +17,97 @@
 from os.path import realpath
 script_directory = Path(realpath(__file__)).parent
 
-def load_config(filename = None):
-    if filename is None:
-        filename = script_directory / 'ldcheck.cfg'
-    from configobj import ConfigObj
-    from copy import deepcopy
-    config = ConfigObj(str(filename), encoding = 'UTF8')
-    read_config = deepcopy(config)
-    if 'libraries' not in config:
-        config['libraries'] = ['/path/to/ldraw']
-    if config != read_config:
-        config.write()
-    check_library_paths(config)
-    load_ldconfig_ldr(config)
-    return config
+def config_dirs():
+    import appdirs
+    appauthor = 'Teemu Piippo'
+    dirs = appdirs.AppDirs(appname, appauthor)
+    return {
+        'user': Path(dirs.user_config_dir),
+        'system': Path(dirs.site_config_dir),
+    }
 
-def library_paths(config):
-    for library_path_string in config['libraries']:
-        yield Path(library_path_string).expanduser()
+def ldraw_dirs_from_config():
+    libraries = []
+    dirs = config_dirs()
+    for dirpath in [dirs['system'], dirs['user']]:
+        filename = dirpath / 'ldcheck.cfg'
+        from configobj import ConfigObj
+        config = ConfigObj(str(filename), encoding = 'UTF8')
+        if 'libraries' in config:
+            libraries = expand_paths(config['libraries'])
+    return libraries
+
+def expand_paths(paths):
+    return [
+        Path(library).expanduser()
+        for library in paths
+    ]
 
-def check_library_paths(config):
-    from sys import exit
-    problems = False
-    have_paths = False
-    for library_path in library_paths(config):
-        have_paths = True
-        if not library_path.exists():
-            problems = True
-            print(str.format(
-                'Library path {} does not exist',
-                library_path,
-            ))
-        elif not library_path.exists():
-            problems = True
-            print(str.format(
-                'Library path {} is not a directory',
-                library_path,
-            ))
-    if not have_paths:
-        print('No LDraw path specified')
-        problems = True
-    if problems:
-        print('Please fix ldcheck.cfg')
-        exit(1)
-
-def find_ldconfig_ldr_paths(config):
-    for library_path in library_paths(config):
-        yield from [
-            library_path / path
-            for path in ['LDConfig.ldr', 'ldconfig.ldr']
-            if (library_path / path).is_file()
-        ]
-
-import argparse
+class LDrawContext:
+    '''
+        Contains context-dependant LDraw information, like library directory
+        paths and the colour table.
+    '''
+    def __init__(self, libraries = None):
+        self._libraries = libraries
+        if not self._libraries:
+            self._libraries = ldraw_dirs_from_config()
+        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
 
 class ListProblemsAction(argparse.Action):
     def __init__(self, option_strings, dest, nargs = None, **kwargs):
@@ -87,11 +125,6 @@
             ))
         exit(0)
 
-def load_ldconfig_ldr(config):
-    for ldconfig_ldr_path in find_ldconfig_ldr_paths(config):
-        with ldconfig_ldr_path.open() as ldconfig_ldr:
-            load_colours(ldconfig_ldr)
-
 def format_report(report, model, test_suite, *, use_colors = True):
     from testsuite import problem_text
     messages = []
@@ -142,6 +175,10 @@
         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}',
@@ -150,7 +187,14 @@
         ),
     )
     args = parser.parse_args()
-    config = load_config()
+    libraries = ldraw_dirs_from_config()
+    if args.ldraw_dir:
+        libraries = expand_paths(args.ldraw_dir)
+    try:
+        context = LDrawContext(libraries)
+    except RuntimeError as error:
+        print('error:', str(error), file = stderr)
+        exit(1)
     if args.color:
         try:
             import colorama
@@ -160,9 +204,7 @@
             args.color = False
     if args.subfile:
         import filecache
-        cache = filecache.SubfileCache(
-            ldraw_directories = config['libraries'],
-        )
+        cache = filecache.SubfileCache(context = context)
         subfile = cache.prepare_file(args.filename)
         if not subfile.valid:
             print(subfile.problem)
@@ -177,7 +219,7 @@
                 model = parse.read_ldraw(
                     file,
                     name = basename(args.filename),
-                    ldraw_directories = config['libraries'])
+                    context = context)
                 if args.dump:
                     print('header: ' + type(model.header).__name__)
                     for key in sorted(dir(model.header)):
--- a/parse.py	Fri Sep 18 21:57:36 2020 +0300
+++ b/parse.py	Fri Sep 18 23:51:45 2020 +0300
@@ -9,13 +9,13 @@
 
 class Model:
     def __init__(
-        self, header, body, *, ldraw_directories, \
+        self, header, body, *, context, \
         header_size = 0, line_ending_errors = None
     ):
         self.header = header
         self.body = body
         self.header_size = header_size
-        self.ldraw_directories = ldraw_directories
+        self.context = context # contains stuff like library paths
         self.line_ending_errors = line_ending_errors
     def filter_by_type(self, type):
         yield from [
@@ -58,7 +58,7 @@
         transformation_matrix = complete_matrix(Matrix3x3(), Vertex(0, 0, 0))
     if file_cache is None:
         import filecache
-        file_cache = filecache.SubfileCache(model.ldraw_directories)
+        file_cache = filecache.SubfileCache(ldraw_directories = model.context.libraries)
     for element in model.body:
         if isinstance(element, linetypes.BasePolygon):
             for point in element.geometry.vertices:
@@ -73,7 +73,7 @@
                 point @= matrix_4x4
                 yield point @ transformation_matrix
 
-def read_ldraw(file, *, ldraw_directories, name = ''):
+def read_ldraw(file, *, context, name = ''):
     line_ending_errors = {
         'count': 0,
         'first-at': None,
@@ -100,7 +100,7 @@
         header = header_object,
         body = model_body,
         header_size = end,
-        ldraw_directories = ldraw_directories,
+        context = context,
         line_ending_errors = line_ending_errors,
     )
     model.name = name
--- a/tests/misc.py	Fri Sep 18 21:57:36 2020 +0300
+++ b/tests/misc.py	Fri Sep 18 23:51:45 2020 +0300
@@ -14,7 +14,7 @@
     from collections import defaultdict
     bad_colours = defaultdict(lambda: {'count': 0, 'first-occurrence': None})
     for element in model.body:
-        if hasattr(element, 'colour') and not element.colour.is_valid:
+        if hasattr(element, 'colour') and not model.context.is_valid_colour(element.colour):
             bad_colours[element.colour.index]['count'] += 1
             if not bad_colours[element.colour.index]['first-occurrence']:
                 bad_colours[element.colour.index]['first-occurrence'] = element
--- a/tests/subfiles.py	Fri Sep 18 21:57:36 2020 +0300
+++ b/tests/subfiles.py	Fri Sep 18 23:51:45 2020 +0300
@@ -162,7 +162,7 @@
         Checks whether flat subfiles are scaled in the flat direction.
     '''
     import filecache
-    cache = filecache.SubfileCache(model.ldraw_directories)
+    cache = filecache.SubfileCache(context = model.context)
     if model.header.valid:
         cache.reference_stack.append(model.header.name)
     failed_subfiles = set()
--- a/unittest.py	Fri Sep 18 21:57:36 2020 +0300
+++ b/unittest.py	Fri Sep 18 23:51:45 2020 +0300
@@ -1,6 +1,5 @@
 #!/usr/bin/env python3
 from ldcheck import appname, version, version_string
-from ldcheck import load_config, find_ldconfig_ldr_paths
 from ldcheck import script_directory
 from pathlib import Path
 from parse import read_ldraw
@@ -21,7 +20,7 @@
     problem_type, line_no_str = str.split(text, ':')
     return problem_type, int(line_no_str)
 
-def load_unit_test(unit_test_path, *, name, ldraw_directories):
+def load_unit_test(unit_test_path, *, name, context):
     with open(unit_test_path, 'rb') as device:
         import re
         problem_types = set()
@@ -56,19 +55,19 @@
             'model': read_ldraw(
                 device,
                 name = name,
-                ldraw_directories = ldraw_directories
+                context = context
             ),
         }
 
 def parse_problem(problem):
     return problem.problem_class.name, problem.line_number
 
-def run_unit_test(unit_test_path, *, config, test_suite):
+def run_unit_test(unit_test_path, *, context, test_suite):
     from os.path import basename
     unit_test = load_unit_test(
         unit_test_path,
         name = basename(unit_test_path),
-        ldraw_directories = config['libraries'],
+        context = context,
     )
     bad_problems = set.difference(
         unit_test['problem_types'],
@@ -106,7 +105,8 @@
     parser = ArgumentParser()
     parser.add_argument('-d', '--debug', action = 'store_true')
     args = parser.parse_args()
-    config = load_config()
+    from ldcheck import LDrawContext
+    context = LDrawContext()
     test_suite = load_tests()
     num_tested = 0
     num_passed = 0
@@ -117,7 +117,7 @@
         try:
             unit_test_report = run_unit_test(
                 unit_test_path,
-                config = config,
+                context = context,
                 test_suite = test_suite
             )
         except Exception as error:
--- a/webfront.py	Fri Sep 18 21:57:36 2020 +0300
+++ b/webfront.py	Fri Sep 18 23:51:45 2020 +0300
@@ -1,12 +1,19 @@
 #!/usr/bin/env python3
 from flask import Flask, render_template, redirect, request
 from ldcheck import appname, version, version_string
-from ldcheck import load_config, find_ldconfig_ldr_paths
 from parse import read_ldraw
 from testsuite import load_tests, check_model, problem_text, all_problem_types
 
 app = Flask(appname)
 
+from ldcheck import LDrawContext
+try:
+    context = LDrawContext()
+except RuntimeError as error:
+    from sys import stderr, exit
+    print('error:', str(error), file = stderr)
+    exit(1)
+
 def format_report_html(report, model, test_suite):
     messages = []
     for problem in report['problems']:
@@ -37,12 +44,7 @@
             return redirect(request.url)
         file = request.files['file']
         filename = file.filename
-        config = load_config()
-        model = read_ldraw(
-            file.stream,
-            name = filename,
-            ldraw_directories = config['libraries'],
-        )
+        model = read_ldraw(file.stream, name = filename, context = context)
         report = check_model(model, test_suite)
 
         # Amend human-readable messages into the report

mercurial