From 3bf2b10ae0bef28677ea0ab3e2d1bbb3fe31735f Mon Sep 17 00:00:00 2001 From: pommicket Date: Tue, 23 Sep 2025 13:31:00 -0400 Subject: Clean up examples, fix a few bugs --- examples/all_functions.py | 88 +++++++++++++++++++++++++++++++++++++++++++++++ examples/read_conf.py | 18 ++++++++-- pom_parser/__init__.py | 57 ++++++++++++++++-------------- pre-commit.sh | 2 ++ tests/location.py | 2 +- tests/parsing.py | 4 +-- 6 files changed, 140 insertions(+), 31 deletions(-) create mode 100644 examples/all_functions.py diff --git a/examples/all_functions.py b/examples/all_functions.py new file mode 100644 index 0000000..b8c3786 --- /dev/null +++ b/examples/all_functions.py @@ -0,0 +1,88 @@ +# Put root of repository in sys.path +# (Ordinarily you won't want to do this — this is only +# needed to make this example work without pom_parser installed.) +from pathlib import Path +import sys +sys.path.append(str(Path(__file__).parent.parent)) + +import pom_parser + +filename = 'examples/conf.pom' if len(sys.argv) < 2 else sys.argv[1] +# Ordinary usage: read configuration from file path +conf = pom_parser.load_path(filename) + +with open(filename, 'rb') as f: + # Can also load directly from file object + conf = pom_parser.load_file(filename, f) + +# Load configuration from string +overrides = pom_parser.load_string('', '''tab-size = 12 +font-size = 15.5 +overrides-applied = yes''') + +# Print all key-value pairs in configuration +print(str(conf)) + + +# Get value of key in configuration +indentation_type = conf.get('indentation-type') +if indentation_type is not None: + # Key is set + print('Indenting with', indentation_type) +else: + # Key is not set + print('No indentation type specified') + +# Get value, or else use default +indentation_type = conf.get('indentation-type', '') +print('Indenting with', indentation_type) + +# Parse value as integer +try: + tab_size = conf.get_int('tab-size', 4) + print('Tab size:', tab_size) +except pom_parser.Error as e: + # tab-size is not set to an integer + print('Error:', e) + +# get_uint doesn't allow negative values +tab_size = conf.get_uint('tab-size', 4) +print('Tab size:', tab_size) + +# Parse value as floating-point number +font_size = conf.get_float('font-size', 12.5) +print('font size:', font_size) + +# Parse value as boolean +show_line_numbers = conf.get_bool('show-line-numbers', True) +print('show line numbers?', 'yes' if show_line_numbers else 'no') + +# Parse value as list +cpp_extensions = conf.get_list('file-extensions.Cpp', ['.cpp', '.hpp']) +print('C++ file extensions:', cpp_extensions) + +# Extract section out of configuration +file_extensions = conf.section('file-extensions') +c_extensions = file_extensions.get_list('C', ['.c', '.h']) +print('C file extensions:', c_extensions) + +plug_ins = conf.section('plug-in') +# Iterate over unique first components of keys +for key in plug_ins.keys(): + # Get location where key was defined + location = plug_ins.location(key) + assert location is not None + _filename, line = location + enabled = plug_ins.get_bool(key + '.enabled', True) + print('Plug-in', key, 'defined at line', line, '(enabled)' if enabled else '(disabled)') + +# Merge configurations (this prefers values in overrides) +overriden = conf.merge(overrides) + +# Iterate over key-value pairs in configuration +for item in overriden: + print(item.key, ':', item.value) + +# Iterate over items which haven't been accessed through .get +for key in conf.unread_keys(): + print('Unknown key:', key) diff --git a/examples/read_conf.py b/examples/read_conf.py index bbee83b..4f6bf56 100644 --- a/examples/read_conf.py +++ b/examples/read_conf.py @@ -6,9 +6,21 @@ import sys sys.path.append(str(Path(__file__).parent.parent)) import pom_parser + +filename = 'examples/conf.pom' if len(sys.argv) < 2 else sys.argv[1] try: - filename = 'examples/conf.pom' if len(sys.argv) < 2 else sys.argv[1] + # Load configuration from file conf = pom_parser.load_path(filename) - print(conf.location('file-extensions')) except pom_parser.Error as e: - print('Parse error:', str(e), sep = '\n') + # Handle error due to invalid configuration file + print('Parse error:\n' + str(e)) + sys.exit(1) + +# Get value of key in configuration +indentation_type = conf.get('indentation-type') +if indentation_type is not None: + # Key is set + print('Indenting with', indentation_type) +else: + # Key is not set + print('No indentation type specified') diff --git a/pom_parser/__init__.py b/pom_parser/__init__.py index 8c86e7b..230ecc1 100644 --- a/pom_parser/__init__.py +++ b/pom_parser/__init__.py @@ -1,6 +1,3 @@ -# TODO: -# - clean up read_conf example -# - add all_functions example r'''Configuration for the [POM configuration file format](https://www.pom.computer). \mainpage pom_parser @@ -51,6 +48,7 @@ Attributes e.next = l[i+1] return l[0] + class Item: r''' An item (key-value pair) in a POM configuration. @@ -65,17 +63,21 @@ Attributes File name where item was defined. - `line: int` - Line number where item was defined. -- `read: bool` - - Has this item been accessed by a \ref pom_parser.Configuration `get_*` method? ''' key: str value: str file: str line: int - read: bool + # This is a list so we can pass it around by reference + _read: list[bool] + def __repr__(self) -> str: return f'' + def read(self) -> bool: + '''Returns whether this item's value has been accessed through `Configuration.get_*`.''' + return self._read[0] + def _error(self, message: str) -> Error: return Error(self.file, self.line, message) @@ -103,6 +105,9 @@ Attributes return None return value + def _set_read(self) -> None: + self._read[0] = True + def _parse_int(self) -> Optional[int]: sign = 1 value = self.value @@ -162,12 +167,19 @@ class Configuration: r'''A POM configuration.''' _items: dict[str, Item] _section_locations: dict[str, tuple[str, int]] - def __repr__(self) -> str: + def __str__(self) -> str: result = [] for item in self._items.values(): - result.append(f'{item.key}: {repr(item.value)}') + result.append(f'{item.key} = {repr(item.value)}') return '\n'.join(result) + def __iter__(self) -> Iterator[Item]: + r'''Get all items (key-value pairs) in configuration. + +The order of the returned items is arbitrary and may change in future versions.''' + import copy + return iter(map(copy.copy, self._items.values())) + def _init(self, items: dict[str, Item]) -> None: self._items = items self._section_locations = {} @@ -200,7 +212,7 @@ class Configuration: item = self._items.get(key) if item is None: return default - item.read = True + item._set_read() return item.value def get_uint(self, key: str, default: Optional[int] = None) -> Optional[int]: @@ -215,7 +227,7 @@ not a valid unsigned integer (< 2^53). item = self._items.get(key) if item is None: return None if default is None else int(default) - item.read = True + item._set_read() uint = item._parse_uint() if uint is None: raise item._error(f'Value {repr(item.value)} for {item.key} is ' @@ -234,7 +246,7 @@ its value is not a valid integer (with absolute value < 2^53). item = self._items.get(key) if item is None: return None if default is None else int(default) - item.read = True + item._set_read() intv = item._parse_int() if intv is None: raise item._error(f'Value {repr(item.value)} for {item.key} is not a valid integer.') @@ -251,7 +263,7 @@ its value is not a valid integer (with absolute value < 2^53). item = self._items.get(key) if item is None: return None if default is None else float(default) - item.read = True + item._set_read() intv = item._parse_float() if intv is None: raise item._error(f'Value {repr(item.value)} for {item.key} is not a valid number.') @@ -268,7 +280,7 @@ its value is not a valid integer (with absolute value < 2^53). item = self._items.get(key) if item is None: return None if default is None else bool(default) - item.read = True + item._set_read() boolv = item._parse_bool() if boolv is None: raise item._error(f'Value {repr(item.value)} for {item.key} is ' @@ -286,17 +298,10 @@ Literal commas can be included in the list by using `\,`. item = self._items.get(key) if item is None: return None if default is None else default - item.read = True + item._set_read() return item._parse_list() - def items(self) -> Iterator[Item]: - r'''Get all items (key-value pairs) in configuration. - -The order of the returned items is arbitrary and may change in future versions.''' - import copy - return iter(map(copy.copy, self._items.values())) - def keys(self) -> Iterator[str]: r'''Get all "direct" keys (unique first components of keys) in configuration. @@ -307,7 +312,7 @@ The order of the returned keys is arbitrary and may change in future versions.'' r'''Get all keys which have not been accessed using a `get_*` method. The order of the returned keys is arbitrary and may change in future versions.''' - return (item.key for item in self._items.values() if not item.read) + return (item.key for item in self._items.values() if not item.read()) def section(self, name: str) -> 'Configuration': r'''Extract a "section" out of a configuration. @@ -318,10 +323,12 @@ with `name.` (with the `name.` stripped out) and their values. import copy section_items = {} name_dot = name + '.' - for item in self.items(): + for item in self: if item.key.startswith(name_dot): item_copy = copy.copy(item) - section_items[item.key[len(name_dot):]] = item_copy + new_key = item.key[len(name_dot):] + item_copy.key = new_key + section_items[new_key] = item_copy conf = Configuration() conf._init(section_items) return conf @@ -513,7 +520,7 @@ class _Parser: key = f'{self.current_section}.{relative_key}' if self.current_section else relative_key item = Item() item.key = key - item.read = False + item._read = [False] item.value = value item.file = self.filename item.line = start_line_number diff --git a/pre-commit.sh b/pre-commit.sh index 601a434..6b8e4bf 100755 --- a/pre-commit.sh +++ b/pre-commit.sh @@ -2,3 +2,5 @@ mypy . || exit 1 pylint pom_parser/__init__.py || exit 1 +which doxygen >/dev/null && { doxygen || exit 1; } +python -m unittest tests || exit 1 diff --git a/tests/location.py b/tests/location.py index cdac8e7..b24d70b 100644 --- a/tests/location.py +++ b/tests/location.py @@ -5,7 +5,7 @@ def test_path(tester: unittest.TestCase, loc_path: str) -> None: conf_path = loc_path.replace('.locations.pom', '.pom') locs = pom_parser.load_path(loc_path) conf = pom_parser.load_path(conf_path) - for item in conf.items(): + for item in conf: expected = locs.get_uint(item.key) tester.assertTrue(expected is not None) tester.assertEqual(expected, item.line, f'Incorrect line number for {item.key}') diff --git a/tests/parsing.py b/tests/parsing.py index 185cde9..52d3283 100644 --- a/tests/parsing.py +++ b/tests/parsing.py @@ -6,10 +6,10 @@ def test_path(tester: unittest.TestCase, flat_path: str) -> None: conf = pom_parser.load_path(conf_path) flat = pom_parser.load_path(flat_path) conf_items = {} - for item in conf.items(): + for item in conf: tester.assertTrue(flat.has(item.key), f'{conf_path} has key {item.key} but {flat_path} does not') conf_items[item.key] = item - for item in flat.items(): + for item in flat: tester.assertTrue(conf.has(item.key), f'{flat_path} has key {item.key} but {conf_path} does not') conf_item = conf_items[item.key] tester.assertEqual(conf_item.value, item.value, f'Values for key {item.key} do not match.') -- cgit v1.2.3