# HG changeset patch # User Teemu Piippo # Date 1600462305 -10800 # Node ID fde18c4d6784f8333af2b08c85368c82107a5da5 # Parent eb4c767522acd9f11f2f860a3ddd4a5f5090ee6a 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. diff -r eb4c767522ac -r fde18c4d6784 colours.py --- 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__': diff -r eb4c767522ac -r fde18c4d6784 filecache.py --- 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"]) diff -r eb4c767522ac -r fde18c4d6784 ldcheck.py --- 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)): diff -r eb4c767522ac -r fde18c4d6784 parse.py --- 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 diff -r eb4c767522ac -r fde18c4d6784 tests/misc.py --- 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 diff -r eb4c767522ac -r fde18c4d6784 tests/subfiles.py --- 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() diff -r eb4c767522ac -r fde18c4d6784 unittest.py --- 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: diff -r eb4c767522ac -r fde18c4d6784 webfront.py --- 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