home · contact · privacy
De-hardcode special treatment of boxes.
authorPlom Heller <plom@plomlompom.com>
Thu, 30 Jul 2026 01:32:00 +0000 (03:32 +0200)
committerPlom Heller <plom@plomlompom.com>
Thu, 30 Jul 2026 01:32:00 +0000 (03:32 +0200)
bricksplom.py

index 8c9671937b4dbc3180cb8ac59d39356c708547a6..acbe7b7fcc8d6bfc3d523f3ae9cd5403fbbdf9e0 100755 (executable)
@@ -9,14 +9,9 @@ from argparse import (
 from dataclasses import dataclass
 from os import environ
 from pathlib import Path
-from typing import Callable, Optional, Self
+from typing import Any, Callable, Optional, Self
 
 NAME_ENV_DIRNAME = 'BRICKSPLOM_DIR'
-PATH_SETS = 'sets.txt'
-PATH_COLORS = 'colors.txt'
-PATH_DESIGNS = 'designs.txt'
-PATH_BRICKS = 'bricks.txt'
-
 NAME_ENV_LDRAW = 'PATH_LDRAW'
 LDRAW_STUDS = {
     'stud.dat',
@@ -144,14 +139,16 @@ class Lookupable:
         'Default single-item display of self.'
         return str(self)
 
-    @property
+    @classmethod
     def matchers(
-            self
-            ) -> dict[str, Callable[[str], bool]]:
+            cls
+            ) -> dict[str, Callable[[Self, str], bool]]:
         'Available lookup matchers.'
         return {
-            PARAM_Q_ID: lambda q_body: self.id_ == q_body,
-            PARAM_Q_TEXT: lambda q_body: q_body.upper() in str(self).upper()
+            PARAM_Q_ID: (lambda item, q_body:
+                         item.id_ == q_body),
+            PARAM_Q_TEXT: (lambda item, q_body:
+                           q_body.upper() in str(item).upper())
             }
 
     def match(
@@ -160,8 +157,7 @@ class Lookupable:
             query_body: str
             ) -> bool:
         'Return if self matches query.'
-        assert query_char in self.matchers, query_char
-        return self.matchers[query_char](query_body)
+        return self.matchers()[query_char](self, query_body)
 
     @classmethod
     def _by_box_sorter(
@@ -187,15 +183,16 @@ class Lookupable:
             return tuple(items + remains)
         return by_box
 
-    @property
+    @classmethod
     def sorters(
-            self
+            cls
             ) -> dict[str, Callable[['BricksDb', tuple[Self, ...]],
                                     tuple[Self, ...]]]:
         'Available sorters.'
         return {
-            TOK_SORT_ID: lambda _, pre_sorted: tuple(
-                sorted(pre_sorted, key=lambda item: item.id_indented()))
+            TOK_SORT_ID: (lambda _, pre_sorted:
+                          tuple(sorted(pre_sorted,
+                                       key=lambda item: item.id_indented())))
             }
 
 
@@ -227,12 +224,12 @@ class BrickColor(Textfiled, Lookupable):
             collected[id_] = cls(id_, desc[0] == CHAR_COL_SOLID, desc[1:])
         return collected
 
-    @property
+    @classmethod
     def sorters(
-            self
+            cls
             ) -> dict[str, Callable[['BricksDb', tuple[Self, ...]],
                                     tuple[Self, ...]]]:
-        return super().sorters | {TOK_SORT_BOX: self._by_box_sorter('color')}
+        return super().sorters() | {TOK_SORT_BOX: cls._by_box_sorter('color')}
 
     def raw(
             self
@@ -282,27 +279,28 @@ class BrickDesign(Textfiled, Lookupable):
         'Own .id_ plus (sorted) .alternate_ids.'
         return tuple([self.id_] + sorted(list(self.alternate_ids)))
 
-    @property
+    @classmethod
     def matchers(
-            self
-            ) -> dict[str, Callable[[str], bool]]:
+            cls
+            ) -> dict[str, Callable[[Self, str], bool]]:
         def attr_matcher(
                 attr_name: str
-                ) -> Callable[[str], bool]:
+                ) -> Callable[[Self, str], bool]:
             def match(
+                    item: Self,
                     q_body: str
                     ) -> bool:
                 assert q_body.isdigit()
-                return getattr(self, attr_name) == int(q_body)
+                return getattr(item, attr_name) == int(q_body)
             return match
-        return super().matchers | {
+        return super().matchers() | {
             f'{attr_name}{CHAR_ATTR_EQ}': attr_matcher(attr_name)
             for attr_name, attr_type in BrickDesignData.__annotations__.items()
             if attr_type is int}
 
-    @property
+    @classmethod
     def sorters(
-            self
+            cls
             ) -> dict[str, Callable[['BricksDb', tuple[Self, ...]],
                                     tuple[Self, ...]]]:
         def attr_sorter(
@@ -315,8 +313,8 @@ class BrickDesign(Textfiled, Lookupable):
                 return tuple(sorted(pre_sorted,
                                     key=lambda item: getattr(item, attr_name)))
             return f_sort
-        return super().sorters\
-            | {TOK_SORT_BOX: self._by_box_sorter('design')}\
+        return super().sorters()\
+            | {TOK_SORT_BOX: cls._by_box_sorter('design')}\
             | {attr_name: attr_sorter(attr_name)
                for attr_name in BrickDesignData.__annotations__}
 
@@ -391,12 +389,12 @@ class Brick(Textfiled, WithDb, Lookupable):
         self.comment = comment
         super().__init__(**kwargs)
 
-    @property
+    @classmethod
     def sorters(
-            self
+            cls
             ) -> dict[str, Callable[['BricksDb', tuple[Self, ...]],
                                     tuple[Self, ...]]]:
-        return super().sorters | {TOK_SORT_BOX: self._by_box_sorter()}
+        return super().sorters() | {TOK_SORT_BOX: cls._by_box_sorter()}
 
     @classmethod
     def from_textfile(
@@ -516,12 +514,13 @@ class BrickSet(Textfiled, WithDb, Lookupable):
             ) -> str:
         return f'{self.id_} {self._is_in_str} {self.description}'
 
-    @property
+    @classmethod
     def matchers(
-            self
-            ) -> dict[str, Callable[[str], bool]]:
-        return super().matchers | {
-            PARAM_Q_TEXT: lambda q_body: q_body.upper() in self.show().upper()
+            cls
+            ) -> dict[str, Callable[[Self, str], bool]]:
+        return super().matchers() | {
+            PARAM_Q_TEXT: (lambda item, q_body:
+                           q_body.upper() in item.show().upper())
             }
 
     def _format_paginated(
@@ -642,7 +641,17 @@ class Box(WithDb, Lookupable):
 
 class BricksDb:
     'Collection of all the tables enabling their combined processing.'
-    lookupable = frozenset({'boxes', 'bricks', 'colors', 'designs', 'sets'})
+    lookupables: dict[str, type[Lookupable]] = {
+            'boxes': Box,
+            'bricks': Brick,
+            'colors': BrickColor,
+            'designs': BrickDesign,
+            'sets': BrickSet
+            }
+    bricks: dict[str, Brick]
+    colors: dict[str, BrickColor]
+    designs: dict[str, BrickDesign]
+    sets: dict[str, BrickSet]
 
     def __init__(
             self,
@@ -650,10 +659,10 @@ class BricksDb:
             path_ldraw: str,
             ldraw_mode: str
             ) -> None:
-        self.colors = BrickColor.from_textfile((path_tables, PATH_COLORS))
-        self.bricks = Brick.from_textfile((path_tables, PATH_BRICKS), db=self)
-        self.sets = BrickSet.from_textfile((path_tables, PATH_SETS), db=self)
-        self.designs = BrickDesign.from_textfile((path_tables, PATH_DESIGNS))
+        for name, cls in self.textfiled_tables().items():
+            kwargs = {'db': self} if issubclass(cls, WithDb) else {}
+            setattr(self, name, cls.from_textfile((path_tables, f'{name}.txt'),
+                                                  **kwargs))
         self._path_ldraw = Path(path_ldraw).joinpath('parts')
         self._ldraw_mode = ldraw_mode.split(',')
         if 'fill' in self._ldraw_mode:
@@ -665,6 +674,14 @@ class BricksDb:
                     design.direct_attrs.ldraw = filename
         self._check_consistencies(verify_ldraw='verify' in self._ldraw_mode)
 
+    @classmethod
+    def textfiled_tables(
+            cls
+            ) -> dict[str, Any]:
+        'Subset of .lookupables of only Textfiled subclasses.'
+        return {key: value for (key, value) in cls.lookupables.items()
+                if issubclass(value, Textfiled)}
+
     @property
     def boxes(
             self
@@ -783,17 +800,18 @@ class BricksDb:
             sort_by: str
             ) -> str:
         'Return result of inquiry on table of table_name.'
-        assert (not show_raw) or table_name != 'boxes'
-        assert hasattr(self, table_name) and table_name in self.lookupable
+        assert table_name in self.lookupables
+        assert (not show_raw) or table_name in self.textfiled_tables()
+        cls = self.lookupables[table_name]
         table = getattr(self, table_name)
         if item_id:
             match_by = PARAM_Q_ID + item_id
         items = tuple(table.values())
         if items and sort_by:
-            items = items[0].sorters[sort_by](self, items)
+            items = cls.sorters()[sort_by](self, items)
         if items and match_by:
             p_key = ''
-            for p_key in [p_key for p_key in items[0].matchers
+            for p_key in [p_key for p_key in cls.matchers()
                           if match_by.startswith(p_key)]:
                 break
             assert p_key
@@ -821,7 +839,7 @@ def main(
                         action,
                         f'{arg} – ambiguous, matches: {", ".join(candidates)}')
                 return (candidates + (arg, ))[0]
-            choices = BricksDb.lookupable
+            choices = BricksDb.lookupables.keys()
             msg = 'one of: ' + ', '.join(sorted(list(choices)))
             msg += '; NB: abbreviable to first unambiguous character sequence'
             action = parser.add_argument(
@@ -841,12 +859,13 @@ def main(
         parser.add_argument(
                 '-l', '--ldraw',
                 action='store', default='')
-        hint_raw = 'not available for boxes table'
+        hint_raw = ('only available for tables: '
+                    + ', '.join(BricksDb.textfiled_tables().keys()))
         action_raw = parser.add_argument(
                 '-r', '--raw',
                 action='store_true', help=f'NB: {hint_raw}')
         args = parser.parse_args()
-        if args.table == 'boxes' and args.raw:
+        if args.table not in BricksDb.textfiled_tables() and args.raw:
             parser.error(str(ArgumentError(action_raw, hint_raw)))
         return args