From e1845776cf3ebe4f5aef0a530fca02290f990cda Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:07:28 +0000 Subject: [PATCH] Bump rose utils --- .github/workflows/checks.yaml | 12 +- .gitignore | 3 + pyproject.toml | 15 +- source/rose_picker/entry.py | 36 +-- source/rose_picker/rose/CONTRIBUTING.md | 90 +++--- source/rose_picker/rose/README.md | 23 +- source/rose_picker/rose/__init__.py | 25 +- source/rose_picker/rose/c3.py | 9 +- source/rose_picker/rose/config.py | 338 ++++++++++++++++------- source/rose_picker/rose/config_tree.py | 112 +++++--- source/rose_picker/rose/env.py | 14 +- source/rose_picker/rose/reporter.py | 46 +-- source/rose_picker/rose/unicode_utils.py | 8 +- tests/rose_picker_test.py | 45 ++- uv.lock | 107 +------ 15 files changed, 488 insertions(+), 395 deletions(-) diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index ae8901d..e69e745 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -53,11 +53,19 @@ jobs: - name: Format Python code if: always() - run: uv run ruff format --check . + run: uv run ruff format --check . --preview - name: Run Python tests if: always() - run: uv run pytest tests + run: uv run pytest -v tests + + - name: Run Python doctests + if: always() + run: uv run pytest -v --doctest-modules + + - name: Check Python type hints + if: always() + run: uv run mypy source/ tests/ - name: Minimise uv cache run: uv cache prune --ci diff --git a/.gitignore b/.gitignore index 4a50265..5dd8fb0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ __pycache__/ *.py[cod] *$py.class +.pytest_cache/ +.mypy_cache/ # C extensions *.so @@ -40,6 +42,7 @@ venv/ .env/ .venv/ env/ +opt/ # Distribution artifacts build/ diff --git a/pyproject.toml b/pyproject.toml index c6ca1b8..bf34dc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ name = "rose_picker" description = "Converts Rose metadata file into JSON format." readme = "README.rst" license = {text = "GPLv3"} -requires-python = ">=3.11" +requires-python = "==3.12.5" dynamic = ["version"] [project.scripts] @@ -14,7 +14,6 @@ rose_picker = "rose_picker.entry:cli" [project.optional-dependencies] dev = [ - "flake8", "mypy", "pytest", "ruff", @@ -31,3 +30,15 @@ version = {attr = "rose_picker.VERSION"} [tool.setuptools.packages.find] where = ["source"] + +[tool.ruff] +line-length = 88 +[tool.ruff.lint] +select = ["E", "F", "W", "B", "I", "UP", "D4"] +ignore = ["I001", "UP031"] +[tool.ruff.lint.per-file-ignores] +"source/rose_picker/rose/*" = ["B", "D", "UP"] +[tool.ruff.format] +quote-style = "double" +indent-style = "space" + diff --git a/source/rose_picker/entry.py b/source/rose_picker/entry.py index 47e44c0..5ef35e7 100755 --- a/source/rose_picker/entry.py +++ b/source/rose_picker/entry.py @@ -25,16 +25,16 @@ import os.path from pathlib import Path import re -from typing import Dict, List, Sequence +from typing import Any +from collections.abc import Sequence -import rose_picker.rose # type: ignore -from rose_picker.rose.config import ConfigSyntaxError # type: ignore -from rose_picker.rose.config_tree import ( # type: ignore - ConfigTree, +import rose_picker.rose +from rose_picker.rose.config import ConfigSyntaxError +from rose_picker.rose.config_tree import ( ConfigTreeLoader, ) -_NAMELISTS_REGEX = re.compile(r"^\s*namelist\s*:\s*(\w*)\s*(?:=\s*(\S+))?") +_NML_REGEX = re.compile(r"^\s*namelist\s*:\s*(\w*)\s*(?:=\s*(\S+))?") class RosePickerException(Exception): @@ -45,7 +45,7 @@ class RosePickerException(Exception): pass # pylint: disable=unnecessary-pass -def _load_configuration(filename: Path, include_dirs: Sequence[Path]) -> ConfigTree: +def _load_configuration(filename: Path, include_dirs: Sequence[Path]) -> Any: """ Load and expand the configuration file. """ @@ -68,31 +68,31 @@ def _load_configuration(filename: Path, include_dirs: Sequence[Path]) -> ConfigT ) from config_syntax -def _list_configuration(config_node: ConfigTree) -> List[str]: +def _list_configuration(config_node: Any) -> list[str]: """ Get keys list of all the namelists/members in the configuration file. """ - node_keys = list(filter(_NAMELISTS_REGEX.match, config_node.get_value().keys())) + node_keys = [key for key in config_node.get_value().keys() if _NML_REGEX.match(key)] node_keys.sort() return node_keys def _extract_namelists( - config_node: ConfigTree, + config_node: Any, namelist_keys: Sequence[str], member_keys: Sequence[str], - listnames: List[str], - namelist_config: Dict[str, Dict[str, Dict[str, Dict[str, str]]]], + listnames: list[str], + namelist_config: dict[str, dict[str, dict[str, dict[str, str]]]], ): # pylint: disable-msg=too-many-locals """ - Extracts namelist properties from meta-data. + Extract namelist properties from meta-data. """ node_keys = _list_configuration(config_node) for key in node_keys: - match = _NAMELISTS_REGEX.match(key) + match = _NML_REGEX.match(key) if match is None: raise Exception(f"Failed to find key in string: {key}") node = match.group(0) @@ -167,8 +167,8 @@ def main(meta_filename: Path, include_dirs: Sequence[Path], output_dir: Path): "values", ] - listnames: List[str] = [] - namelist_config: Dict[str, Dict[str, Dict[str, Dict[str, str]]]] = ( + listnames: list[str] = [] + namelist_config: dict[str, dict[str, dict[str, dict[str, str]]]] = ( collections.OrderedDict() ) _extract_namelists( @@ -179,11 +179,11 @@ def main(meta_filename: Path, include_dirs: Sequence[Path], output_dir: Path): # Output as .json file nml_config_filename = f"{basename[0]}.json" - with open(f"{output_dir}/{nml_config_filename}", "wt", encoding="utf-8") as output: + with open(f"{output_dir}/{nml_config_filename}", "w", encoding="utf-8") as output: json.dump(namelist_config, output, indent=4, ensure_ascii=True) # Write out namelists in configuration - with open(f"{output_dir}/config_namelists.txt", "wt", encoding="utf-8") as output: + with open(f"{output_dir}/config_namelists.txt", "w", encoding="utf-8") as output: for listname in listnames: output.write(f"{listname}\n") diff --git a/source/rose_picker/rose/CONTRIBUTING.md b/source/rose_picker/rose/CONTRIBUTING.md index 15a74b1..59c92a3 100644 --- a/source/rose_picker/rose/CONTRIBUTING.md +++ b/source/rose_picker/rose/CONTRIBUTING.md @@ -7,15 +7,31 @@ Report bugs and request enhancement by opening an issue on bug, add a recipe for repeating it. If requesting an enhancement, describe the use case in detail. +## New Contributors + +Please read the [CLA](#contributor-licence-agreement-and-certificate-of-origin). + +Please add your name to the +[Code Contributors](#code-contributors) section of this file as part of your +first Pull Request (for each Cylc repository you contribute to). + ## Contribute Code -All contributions to Rose are made via pull requests against the *master* -branch of [metomi/rose](https://github.com/metomi/rose). New contributors -should add their details to the [Code Contributors](#code-contributors) -section of this file as part of their first request. The developer who -reviews each pull request is responsible for checking that the -contributor's name is listed in this file before merging the pull request -into *master*. +We use [semver](https://semver.org/) to separate riskier changes (e.g. new features +& code refactors) from bugfixes to provide more stable releases for production environments. + +**Enhancements** are made on the `master` branch and released in the next minor version +(e.g. 2.1, 2.2, 2.3). + +**Bugfixes** and minor usability enhancements are made on bugfix branches and +released as the next maintenance version (e.g. 2.0.1, 2.0.2, 2.0.3). E.G. if the issue is on a `2.0` milestone, branch off of `2.0.x` to +develop your bugfix, then raise the pull request against the `2.0.x` branch. We will later merge the `2.0.x` branch into `master`. + +We use [towncrier](https://towncrier.readthedocs.io/en/stable/index.html) for +generating the changelog. Changelog entries are added by running +``` +towncrier create ..md --content "Short description" +``` ## Code Contributors @@ -23,30 +39,42 @@ The following people have contributed to this code under the terms of the Contributor Licence Agreement and Certificate of Origin detailed below: -* Sadie Bartholomew (Met Office, UK) -* Andrew Clark (Met Office, UK) -* Kerry Day (Met Office, UK) -* Martin Dix (CSIRO, Australia) -* Ben Fitzpatrick (Met Office, UK) -* Craig MacLachlan (Met Office, UK) -* Joseph Mancell (Met Office, UK) -* Dave Matthews (Met Office, UK) -* Hilary Oliver (National Institute of Water and Atmospheric Research, New Zealand) -* Annette Osprey (NCAS Computational Modelling Services, UK) -* Stephen Oxley (Met Office, UK) -* Matt Pryor (Met Office, UK) -* Oliver Sanders (Met Office, UK) -* Jon Seddon (Met Office, UK) -* Harry Shepherd (Met Office, UK) -* Matt Shin (Met Office, UK) -* Tomasz Trzeciak (Met Office, UK) -* Stuart Whitehouse (Met Office, UK) -* Steve Wardle (Met Office, UK) -* Scott Wales (ARC Centre of Excellence for Climate Systems Science, Australia) -* Thomas Coleman (Bureau of Meteorology, Australia) -* Bruno P. Kinoshita (National Institute of Water and Atmospheric Research, New Zealand) -* Tim Pillinger (Met Office, UK) -* Mel Hall (Met Office, UK) + + - Sadie Bartholomew (Met Office, UK) + - Andrew Clark (Met Office, UK) + - Kerry Day (Met Office, UK) + - Martin Dix (CSIRO, Australia) + - Ben Fitzpatrick (Met Office, UK) + - Craig MacLachlan (Met Office, UK) + - Joseph Mancell (Met Office, UK) + - Dave Matthews (Met Office, UK) + - Hilary Oliver (National Institute of Water and Atmospheric Research, New Zealand) + - Annette Osprey (NCAS Computational Modelling Services, UK) + - Stephen Oxley (Met Office, UK) + - Matt Pryor (Met Office, UK) + - Oliver Sanders (Met Office, UK) + - Jon Seddon (Met Office, UK) + - Harry Shepherd (Met Office, UK) + - Matt Shin (Met Office, UK) + - Tomasz Trzeciak (Met Office, UK) + - Stuart Whitehouse (Met Office, UK) + - Steve Wardle (Met Office, UK) + - Scott Wales (ARC Centre of Excellence for Climate Systems Science, Australia) + - Thomas Coleman (Bureau of Meteorology, Australia) + - Declan Valters (Met Office, UK) + - Paul Cresswell (Met Office, UK) + - Bruno P. Kinoshita (National Institute of Water and Atmospheric Research, New Zealand) + - Tim Pillinger (Met Office, UK) + - Mel Hall (Met Office, UK) + - Ronnie Dutta (Met Office, UK) + - Roddy Sharp (Met Office, UK) + - Mark Dawson (Met Office, UK) + - Joe Marsh Rossney (UK Centre for Ecology & Hydrology) + - Dimitrios Theodorakis (Met Office, UK) + - Joseph Abram (Met Office, UK) + - James Frost (Met Office, UK) + - Christopher Bennett (Met Office, UK) + (All contributors are identifiable with email addresses in the version control logs or otherwise.) diff --git a/source/rose_picker/rose/README.md b/source/rose_picker/rose/README.md index 1ee2188..3c2d1e2 100644 --- a/source/rose_picker/rose/README.md +++ b/source/rose_picker/rose/README.md @@ -1,21 +1,24 @@ # Rose -[![Build Status](https://travis-ci.org/metomi/rose.svg?branch=master)](https://travis-ci.org/metomi/rose) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/ad021a33e7a64b398f792305dd901795)](https://www.codacy.com/app/metomi/rose?utm_source=github.com&utm_medium=referral&utm_content=metomi/rose&utm_campaign=Badge_Grade) +[![test](https://github.com/metomi/rose/actions/workflows/test.yml/badge.svg)](https://github.com/metomi/rose/actions/workflows/test.yml) [![DOI](https://zenodo.org/badge/6223866.svg)](https://zenodo.org/badge/latestdoi/6223866) -[![codecov](https://codecov.io/gh/metomi/rose/branch/master/graph/badge.svg)](https://codecov.io/gh/metomi/rose) Rose: a framework for managing and running meteorological suites. ### Python 2 or Python 3 ? -Currently in the source code repository: - - **master branch:** Python 3, **no GUI** - **Rose-2/Cylc-8 Work In Progress** - - **2019.01.x branch:** Python 2, PyGTK GUI - **Rose-1/Cylc-7 Maintenance** +#### Rose 2019 -The first official Rose-2/Cylc-8 release (with a new web UI) is not expected -until late 2019. Until then we recommend the latest rose-2019.01.x/cylc-7.8.x -release for production use. +- Python 2 +- PyGTK GUI +- `2019.01.x` branch in the source code + +#### Rose 2 + +- Python 3 +- PyGObject GUI +- Web-based GUIs will follow in later Rose 2 releases +- `master` branch in the source code [Installation](http://metomi.github.io/rose/doc/html/installation.html) | [User Guide](http://metomi.github.io/rose/) | @@ -23,7 +26,7 @@ release for production use. ## Copyright and Terms of Use -Copyright (C) 2012-2019 British Crown (Met Office) & Contributors +Copyright (C) 2012-2026 British Crown (Met Office) & Contributors Rose is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/source/rose_picker/rose/__init__.py b/source/rose_picker/rose/__init__.py index 5832e0c..910ccd5 100644 --- a/source/rose_picker/rose/__init__.py +++ b/source/rose_picker/rose/__init__.py @@ -1,8 +1,4 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (C) 2012-2019 British Crown (Met Office) & Contributors. -# +# Copyright (C) British Crown (Met Office) & Contributors. # This file is part of Rose, a framework for meteorological suites. # # Rose is free software: you can redistribute it and/or modify @@ -28,7 +24,12 @@ CONFIG_DELIMITER = "=" # Filenames and directory names -CONFIG_NAMES = ["rose-app.conf", "rose-meta.conf", "rose-suite.conf", "rose-suite.info"] +CONFIG_NAMES = [ + "rose-app.conf", + "rose-meta.conf", + "rose-suite.conf", + "rose-suite.info", +] GLOB_CONFIG_FILE = "rose*.conf" META_CONFIG_NAME = "rose-meta.conf" CONFIG_META_DIR = "meta" @@ -69,16 +70,6 @@ "file:", "poll", ] -TOP_CONFIG_DEFAULT_META_IDS = [ - "file:", - "jinja2:suite.rc", - "=meta", - "=opts", - "=root-dir", - "=root-dir{share}", - "=root-dir{share/cycle}", - "=root-dir{work}", -] CONFIG_SETTING_INDEX_DEFAULT = "1" @@ -153,4 +144,4 @@ # Paths in the Rose distribution. FILEPATH_README = "README.md" -__version__ = "2.0a1" +__version__ = "2.7.0" diff --git a/source/rose_picker/rose/c3.py b/source/rose_picker/rose/c3.py index c07166d..b20cc89 100644 --- a/source/rose_picker/rose/c3.py +++ b/source/rose_picker/rose/c3.py @@ -1,8 +1,5 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (C) 2012-2019 British Crown (Met Office) & Contributors. -# +#!/usr/bin/env python3 +# Copyright (C) British Crown (Met Office) & Contributors. # This file is part of Rose, a framework for meteorological suites. # # Rose is free software: you can redistribute it and/or modify @@ -104,7 +101,7 @@ def mro(target_name, get_base_names, *args, **kwargs): return results[target_name] -class _Test(object): +class _Test: """Self tests. Print results in TAP format. Ordering obtained from http://www.python.org/download/releases/2.3/mro/. diff --git a/source/rose_picker/rose/config.py b/source/rose_picker/rose/config.py index 75d1ec0..4911525 100644 --- a/source/rose_picker/rose/config.py +++ b/source/rose_picker/rose/config.py @@ -1,8 +1,4 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (C) 2012-2019 British Crown (Met Office) & Contributors. -# +# Copyright (C) British Crown (Met Office) & Contributors. # This file is part of Rose, a framework for meteorological suites. # # Rose is free software: you can redistribute it and/or modify @@ -115,7 +111,7 @@ OPT_CONFIG_SETTING_COMMENT = ' setting from opt config "%s" (%s)' -class ConfigNode(object): +class ConfigNode: """Represent a node in a configuration file. Nodes are stored hierarchically, for instance the following config @@ -187,9 +183,11 @@ def __init__(self, value=None, state=STATE_NORMAL, comments=None): self.comments = comments def __repr__(self): - return str( - {"value": self.value, "state": self.state, "comments": self.comments} - ) + return str({ + "value": self.value, + "state": self.state, + "comments": self.comments, + }) __str__ = __repr__ @@ -306,10 +304,10 @@ def get(self, keys=None, no_ignore=False): """Return a node at the position of keys, if any. Args: - keys (list, optional): A list defining a hierarchy of + keys (list): A list defining a hierarchy of node.value 'keys'. If an entry in keys is the null string, it is skipped. - no_ignore (bool, optional): If True any ignored nodes will + no_ignore (bool): If True any ignored nodes will not be returned. Returns: @@ -388,12 +386,12 @@ def get_value(self, keys=None, default=None): If the node does not exist or is ignored, return None. Args: - keys (list, optional): A list defining a hierarchy of node.value + keys (list): A list defining a hierarchy of node.value 'keys'. If an entry in keys is the null string, it is skipped. - default (obj, optional): Return default if the value is not set. + default (object): Return default if the value is not set. Returns: - obj: The value of this ConfigNode at the position of keys or + object: The value of this ConfigNode at the position of keys or default if not set. Examples: @@ -432,7 +430,7 @@ def set(self, keys=None, value=None, state=None, comments=None): Arguments: keys (list): A list defining a hierarchy of node.value 'keys'. If an entry in keys is the null string, it is skipped. - value (obj): The node.value property to set at this position. + value (object): The node.value property to set at this position. state (str): The node.state property to set at this position. If None, the node.state property is unchanged. comments (str): The node.comments property to set at this position. @@ -573,7 +571,12 @@ def add(self, config_diff): subnode.state = state subnode.comments = comments else: - self.set(keys=modified_key, value=value, state=state, comments=comments) + self.set( + keys=modified_key, + value=value, + state=state, + comments=comments, + ) for removed_key, _ in config_diff.get_removed(): self.unset(keys=removed_key) @@ -620,7 +623,8 @@ def __sub__(self, other_config_node): """Produce a ConfigNodeDiff from another ConfigNode. Arguments: - other_config_node - The ConfigNode to be applied to this ConfigNode + other_config_node (ConfigNode): + The ConfigNode to be applied to this ConfigNode to produce the ConfigNodeDiff. Returns: @@ -651,7 +655,11 @@ def __getstate__(self): This avoids a read-only error and allows __slots__ compatibility. """ - return {"state": self.state, "value": self.value, "comments": self.comments} + return { + "state": self.state, + "value": self.value, + "comments": self.comments, + } def __setstate__(self, state): """Read in the results of __getstate__.""" @@ -660,7 +668,7 @@ def __setstate__(self, state): self.comments = state["comments"] -class ConfigNodeDiff(object): +class ConfigNodeDiff: """Represent differences between two ConfigNode instances. Examples: @@ -705,7 +713,11 @@ class ConfigNodeDiff(object): KEY_REMOVED = "removed" def __init__(self): - self._data = {self.KEY_ADDED: {}, self.KEY_REMOVED: {}, self.KEY_MODIFIED: {}} + self._data = { + self.KEY_ADDED: {}, + self.KEY_REMOVED: {}, + self.KEY_MODIFIED: {}, + } def set_from_configs(self, config_node_1, config_node_2): """Create diff data from two ConfigNode instances. @@ -751,25 +763,62 @@ def set_from_configs(self, config_node_1, config_node_2): if settings_1[keys] != settings_2[keys]: self.set_modified_setting(keys, settings_1[keys], settings_2[keys]) - def get_as_opt_config(self): + def get_as_opt_config(self, base_config=None): """Return a ConfigNode such that main + new_node = main + diff. Add all the added settings, add all the modified settings, add all the removed settings as user-ignored. + Args: + base_config: If a setting is present in the diff, but the parent + of the setting is not, then the state and keys of the parent + will revert to default. If "base_config" is provided, then + the state and comments will be transferred from this config + into the generated opt conf. + Returns: ConfigNode: A new ConfigNode instance. Example: - >>> config_node_diff = ConfigNodeDiff() - >>> config_node_diff.set_added_setting(['foo'], - ... ('Foo', None, None,)) - >>> config_node_diff.set_removed_setting(['bar'], - ... ('Bar', None, None,)) - >>> config_node = config_node_diff.get_as_opt_config() - >>> list(config_node.walk()) # doctest: +NORMALIZE_WHITESPACE - [(['', 'bar'], {'value': 'Bar', 'state': '!', 'comments': []}), - (['', 'foo'], {'value': 'Foo', 'state': '', 'comments': []})] + >>> # create a config + >>> a = ( + ... ConfigNode() + ... .set(('x'), state='!!', comments=['foo']) + ... .set(('x', 'a'), '1') + ... .set(('x', 'b'), '3') + ... ) + + >>> # create another config + >>> b = ( + ... ConfigNode() + ... .set(('x'), state='!!', comments=['foo']) + ... .set(('x', 'a'), '2') + ... ) + + >>> # calculate the diff between them + >>> diff = a - b + + >>> # inspect the diff + >>> diff.get_added() + [(('x', 'b'), ('3', '', []))] + >>> diff.get_modified() + [(('x', 'a'), (('2', '', []), ('1', '', [])))] + + # generate the diff as an optional config + # (note the state of "[x]" has been lost as it is not present in + # the diff) + >>> dump(diff.get_as_opt_config(), sys.stdout) + [x] + a=1 + b=3 + + # generate the diff as an optional config supplying the base_config + # (note the state of "[x]" has been extracted from config "a") + >>> dump(diff.get_as_opt_config(a), sys.stdout) + #foo + [!!x] + a=1 + b=3 """ node = ConfigNode() @@ -783,17 +832,43 @@ def get_as_opt_config(self): # Need to add as user-ignored. value, state, comments = info node.set( - keys, value=value, state=node.STATE_USER_IGNORED, comments=comments + keys, + value=value, + state=node.STATE_USER_IGNORED, + comments=comments, ) + + if base_config: + all_keys = { + *(keys for keys, _ in self.get_added()), + *(keys for keys, _ in self.get_modified()), + } + all_parent_keys = set() + for keys in all_keys: + keys = tuple(keys) + while len(keys) > 1: + keys = keys[:-1] + all_parent_keys.add(keys) + + for keys in all_parent_keys - all_keys: + parent = base_config.get(keys) + if parent: + ret = node.get(keys) + ret.state = parent.state + ret.comments = parent.comments + return node def set_added_setting(self, keys, data): """Set a config setting to be "added" in this ConfigNodeDiff. Args: - keys (list/tuple): The position of the setting to add. - data (obj, str, str): A tuple (value, state, comments) for the - setting to add. + keys (list, tuple): + The position of the setting to add. + data (tuple): + A tuple of the form + ``(value: object, state: string, comments: string)`` + for the setting to add. Examples: >>> config_node_diff = ConfigNodeDiff() @@ -822,11 +897,14 @@ def set_modified_setting(self, keys, old_data, data): None then no change will be made to any pre-existing value. Args: - keys (list/tuple): The position of the setting to add. - old_data (obj, str, str): A tuple (value, state, comments) for - the "current" properties of the setting to modify. - data (obj, str, str): A tuple (value, state, comments) for "new" - properties to change this setting to. + keys (list, tuple): + The position of the setting to add. + old_data (tuple): + A tuple ``(value: object, state: str, comments: str)`` + for the "current" properties of the setting to modify. + data (object): + A tuple ``(value: object, state: str, comments: str)`` + for "new" properties to change this setting to. Examples: >>> # Create a ConfigNodeDiff. @@ -852,8 +930,10 @@ def set_removed_setting(self, keys, data): """Set a config setting to be "removed" in this ConfigNodeDiff. Arguments: - keys (list): The position of the setting to add. - data (obj, str, str): A tuple (value, state, comments) of the + keys (list): + The position of the setting to add. + data (tuple): + A tuple ``(value: object, state: str, comments: str)`` of the properties for the setting to remove. Example: @@ -928,10 +1008,10 @@ def get_removed(self): set to None for sections. Returns: - list - A list of the form [(keys, data), ...]: - - keys - The position of an added setting. - - data - Tuple of the form (value, state, comments) of the - properties of the removed setting. + list: A list of the form ``[(keys, data), ...]``: + - keys - The position of an added setting. + - data - Tuple of the form (value, state, comments) of the + properties of the removed setting. Examples: >>> config_node_diff = ConfigNodeDiff() @@ -1007,7 +1087,7 @@ def delete_removed(self): self._data[self.KEY_REMOVED] = {} -class ConfigDumper(object): +class ConfigDumper: """Dumper of a ConfigNode object in Rose INI format. Examples: @@ -1046,17 +1126,17 @@ def dump( Args: root (ConfigNode): The root config node. - target (str/file): An open file handle or a string containing a + target (object): An open file handle or a string containing a file path. If not specified, the result is written to sys.stdout. - sort_sections (fcn - optional): An optional argument that should be + sort_sections (Callable): An optional argument that should be a function for sorting a list of section keys. - sort_option_items (fcn - optional): An optional argument that + sort_option_items (Callable): An optional argument that should be a function for sorting a list of option (key, value) tuples in string values. - env_escape_ok (bool - optional): An optional argument to indicate + env_escape_ok (bool): An optional argument to indicate that $NAME and ${NAME} syntax in values should be escaped. - concat_mode (bool - optional): Switch on concatenation mode. If + concat_mode (bool): Switch on concatenation mode. If True, add [] before root level options. """ @@ -1072,7 +1152,10 @@ def dump( if not os.path.isdir(target_dir): os.makedirs(target_dir) handle = NamedTemporaryFile( - mode="w", prefix=os.path.basename(target), dir=target_dir, delete=False + mode="w", + prefix=os.path.basename(target), + dir=target_dir, + delete=False, ) blank = "" if root.comments: @@ -1156,7 +1239,7 @@ def _comment_format(cls, comment): return "#%s\n" % (comment) -class ConfigLoader(object): +class ConfigLoader: """Loader of an INI format configuration into a ConfigNode object. Example: @@ -1181,18 +1264,25 @@ class ConfigLoader(object): TYPE_OPTION = "TYPE_OPTION" UNKNOWN_NAME = "" - def __init__(self, char_assign=CHAR_ASSIGN, char_comment=CHAR_COMMENT): + def __init__( + self, + char_assign=CHAR_ASSIGN, + char_comment=CHAR_COMMENT, + allow_sections=True, + ): """Initialise the configuration utility. Arguments: - char_comment -- the character to indicate the start of a - comment. - char_assign -- the character to use to delimit a key=value - assignment. + char_assign (str): the character to use to delimit a key=value + assignment. + char_comment (str): the character to indicate the start of a + comment. + allow_sections (bool): whether to permit sections in the config. """ self.char_assign = char_assign self.char_comment = char_comment + self.allow_sections = allow_sections self.re_option = re.compile( r"^(?P!?!?)(?P