anna
Advanced tools
Sorry, the diff of this file is not supported yet
| Metadata-Version: 1.1 | ||
| Name: anna | ||
| Version: 0.4 | ||
| Summary: A Neat configuratioN Auxiliary | ||
| Home-page: https://gitlab.com/Dominik1123/Anna | ||
| Author: Dominik Vilsmeier | ||
| Author-email: dominik.vilsmeier1123@gmail.com | ||
| License: BSD-3-Clause | ||
| Description: Anna - A Neat configuratioN Auxiliary | ||
| ===================================== | ||
| Anna helps you configure your application by building the bridge between the components of | ||
| your application and external configuration sources. It allows you to keep your code short and | ||
| flexible yet explicit when it comes to configuration - the necessary tinkering is performed by | ||
| the framework. | ||
| Anna contains lots of "in-place" documentation aka doc strings so make sure you check out those | ||
| too ("``help`` yourself")! | ||
| 80 seconds to Anna | ||
| ------------------ | ||
| Anna is all about *parameters* and *configuration sources*. You declare parameters as part of | ||
| your application (on a class for example) and specify their values in a configuration source. | ||
| All you're left to do with then is to point your application to the configuration source and | ||
| let the framework do its job. | ||
| An example is worth a thousand words | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| Say we want to build an application that deals with vehicles. I'm into cars so the first thing | ||
| I'll do is make sure we get one of those:: | ||
| >>> class Car: | ||
| ... def __init__(self, brand, model): | ||
| ... self._brand = brand | ||
| ... self._model = model | ||
| >>> | ||
| >>> your_car = Car('Your favorite brand', 'The hottest model') | ||
| Great! We let the user specify the car's ``brand`` and ``model`` and return him a brand new car! | ||
| Now we're using ``anna`` for declaring the parameters:: | ||
| >>> from anna import Configurable, parametrize, String, JSONAdaptor | ||
| >>> | ||
| >>> @parametrize( | ||
| ... String('Brand'), | ||
| ... String('Model') | ||
| ... ) | ||
| ... class Car(Configurable): | ||
| ... def __init__(self, config): | ||
| ... super(Car, self).__init__(config) | ||
| >>> | ||
| >>> your_car = Car(JSONAdaptor('the_file_where_you_specified_your_favorite_car.json')) | ||
| The corresponding json file would look like this:: | ||
| { | ||
| "Car/Parameters/Brand": "Your favorite brand", | ||
| "Car/Parameters/Model": "The hottest model", | ||
| } | ||
| It's a bit more to type but this comes at a few advantages: | ||
| * We can specify the type of the parameter and ``anna`` will handle the necessary conversions | ||
| for us; ``anna`` ships with plenty of parameter types so there's much more to it than just | ||
| strings! | ||
| * If we change your mind later on and want to add another parameter, say for example the color | ||
| of the car, it's as easy as declaring a new parameter ``String('Color')`` and setting it as | ||
| a class attribute; all the user needs to do is to specify the corresponding value in | ||
| the configuration source. Note that there's no need to change any interfaces/signatures or | ||
| other intermediate components which carry the user input to the receiving class; all it expects | ||
| is a configuration adaptor which points to the configuration source. | ||
| * The configuration source can host parameters for more than only one component, meaning again | ||
| that we don't need to modify intermediate parts when adding new components to our application; | ||
| all we need to do is provide the configuration adaptor. | ||
| Five minutes hands-on | ||
| --------------------- | ||
| The 80 seconds intro piqued your curiosity? Great! So let's move on! For the following | ||
| considerations we'll pick up the example from above and elaborate on it more thoroughly. | ||
| Let's start with a quick Q/A session | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| **So what happened when using the decorator ``parametrize``?** It received a number of parameters | ||
| as arguments which it set as attributes on the receiving class. Field names are deduced from | ||
| the parameters names applying CamelCase to _snake_case_with_leading_underscore conversion. | ||
| That is ``String('Brand')`` is set as ``Car._brand``. | ||
| **All right, but how did the instance receive its values then?** Note that ``Car`` inherits from | ||
| ``Configurable`` and ``Configurable.__init__`` is where the actual instance configuration happens. | ||
| We provided it a configuration adaptor which points to the configuration source (in this case | ||
| a local file) and the specific values were extracted from there. Values are set on the instance | ||
| using the parameter's field name, that is ``String('Brand')`` will make an instance receive | ||
| the corresponding value at ``your_car._brand`` (``Car._brand`` is still the parameter instance). | ||
| **Okay, but how did the framework know where to find the values in the configuration source?** | ||
| Well there's a bit more going on during the call to ``parametrize`` than is written above. | ||
| In addition to setting the parameters on the class it also deduces a configuration path for | ||
| each parameter which specifies where to find the corresponding value in the source. The path | ||
| consists of a base path and the parameter's name: "<base-path>/<name>" (slashes are used | ||
| to delimit path elements). ``parametrize`` tries to get this base path from the receiving class | ||
| looking up the attribute ``CONFIG_PATH``. If it has no such attribute or if it's ``None`` then | ||
| the base path defaults to "<class-name>/Parameters". However in our example - although we didn't | ||
| set the config path explicitly - it was already there because ``Configurable`` uses a custom | ||
| metaclass which adds the class attribute ``CONFIG_PATH`` if it's missing or ``None`` using | ||
| the same default as above. So if you want to specify a custom path within the source you can do so | ||
| by specifying the class attribute ``CONFIG_PATH``. | ||
| **_snake_case_with_leading_underscore, not too bad but can I choose custom field names for the parameters too?** | ||
| Yes, besides providing a number of parameters as arguments to ``parametrize`` we have the option | ||
| to supply it a number of keyword arguments as well which represent field_name / parameter pairs; | ||
| the key is the field name and the value is the parameter: ``brand_name=String('Brand')``. | ||
| **Now that we declared all those parameters how does the user know what to specify?** | ||
| ``anna`` provides a decorator ``document_parameters`` which will add all declared parameters to | ||
| the component's doc string under a new section. Another option for the user is to retrieve | ||
| the declared parameters via ``get_parameters`` (which is inherited from ``Configurable``) and | ||
| print their string representations which contain comprehensive information:: | ||
| >>> for parameter in Car.get_parameters(): | ||
| ... print(parameter) | ||
| Of course documenting the parameters manually is also an option. | ||
| Alright so let's get to the code | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| :: | ||
| >>> from anna import Configurable, parametrize, String, JSONAdaptor | ||
| >>> | ||
| >>> @parametrize( | ||
| ... String('Model'), | ||
| ... brand_name=String('Brand') | ||
| ... ) | ||
| ... class Car(Configurable): | ||
| ... CONFIG_PATH = 'Car' | ||
| ... def __init__(self, config): | ||
| ... super(Car, self).__init__(config) | ||
| Let's first see what information we can get about the parameters:: | ||
| >>> for parameter in Car.get_parameters(): | ||
| ... print(parameter) | ||
| ... | ||
| { | ||
| "optional": false, | ||
| "type": "StringParameter", | ||
| "name": "Model", | ||
| "path": "Car" | ||
| } | ||
| { | ||
| "optional": false, | ||
| "type": "StringParameter", | ||
| "name": "Brand", | ||
| "path": "Car" | ||
| } | ||
| Note that it prints ``"StringParameter"`` because that's the parameter's actual class, | ||
| ``String`` is just a shorthand. Let's see what we can get from the doc string:: | ||
| >>> print(Car.__doc__) | ||
| None | ||
| >>> from anna import document_parameters | ||
| >>> Car = document_parameters(Car) | ||
| >>> print(Car.__doc__) | ||
| Declared parameters | ||
| ------------------- | ||
| (configuration path: Car) | ||
| Brand : String | ||
| Model : String | ||
| Now that we know what we need to specify let's get us a car! The ``JSONAdaptor`` can also be | ||
| initialized with a ``dict`` as root element, so we're just creating our configuration on the fly:: | ||
| >>> back_to_the_future = JSONAdaptor(root={ | ||
| ... 'Car/Brand': 'DeLorean', | ||
| ... 'Car/Model': 'DMC-12', | ||
| ... }) | ||
| >>> doc_browns_car = Car(back_to_the_future) | ||
| >>> doc_browns_car.brand_name # Access via our custom field name. | ||
| 'DeLorean' | ||
| >>> doc_browns_car._model # Access via the automatically chosen field name. | ||
| 'DMC-12' | ||
| Creating another car is as easy as providing another configuration source:: | ||
| >>> mr_bonds_car = Car(JSONAdaptor(root={ | ||
| ... 'Car/Brand': 'Aston Martin', | ||
| ... 'Car/Model': 'DB5', | ||
| ... })) | ||
| Let's assume we want more information about the brand than just its name. We have nicely stored | ||
| all information in a database:: | ||
| >>> database = { | ||
| ... 'DeLorean': { | ||
| ... 'name': 'DeLorean', | ||
| ... 'founded in': 1975, | ||
| ... 'founded by': 'John DeLorean', | ||
| ... }, | ||
| ... 'Aston Martin': { | ||
| ... 'name': 'Aston Martin', | ||
| ... 'founded in': 1913, | ||
| ... 'founded by': 'Lionel Martin, Robert Bamford', | ||
| ... }} | ||
| We also have a database access function which we can use to load stuff from the database:: | ||
| >>> def load_from_database(key): | ||
| ... return database[key] | ||
| To load this database information instead of just the brand's name we only have to modify | ||
| the ``Car`` class to declare a new parameter: ``ActionParameter`` (or ``Action``). | ||
| An ``ActionParameter`` wraps another parameter and let's us specify an action which is applied to | ||
| the parameter's value when it's loaded. For our case that is:: | ||
| >>> from anna import ActionParameter | ||
| >>> Car.brand = ActionParameter(String('Brand'), load_from_database) | ||
| >>> doc_browns_car = Car(back_to_the_future) | ||
| >>> doc_browns_car.brand | ||
| {'founded by': 'John DeLorean', 'name': 'DeLorean', 'founded in': 1975} | ||
| >>> doc_browns_car.brand_name | ||
| 'DeLorean' | ||
| Note that we didn't need to provide a new configuration source as the new ``brand`` parameter is | ||
| based on the brand name which is already present. | ||
| Say we also want to obtain the year in which the model was first produced and we have a function | ||
| for exactly that purpose however it requires the brand name and model name as one string:: | ||
| >>> def first_produced_in(brand_and_model): | ||
| ... return {'DeLorean DMC-12': 1981, 'Aston Martin DB5': 1963}[brand_and_model] | ||
| That's not a problem because an ``ActionParameter`` type lets us combine multiple parameters:: | ||
| >>> Car.first_produced_in = ActionParameter( | ||
| ... String('Brand'), | ||
| ... lambda brand, model: first_produced_in('%s %s' % (brand, model)), | ||
| ... depends_on=('Model',)) | ||
| Other existing parameters, specified either by name of by reference via the keyword argument | ||
| ``depends_on``, are passed as additional arguments to the given action. | ||
| In the above example we declared parameters on a class using ``parametrize`` but you could as well | ||
| use parameter instances independently and load their values via ``load_from_configuration`` which | ||
| expects a configuration adaptor as well as a configuration path which localizes the parameter's | ||
| value. You also have the option to provide a specification directly via | ||
| ``load_from_representation``. This functions expects the specification as a unicode string and | ||
| additional (meta) data as a ``dict`` (a unit for ``PhysicalQuantities`` for example). | ||
| This introduction was meant to demonstrate the basic principles but there's much more to ``anna`` | ||
| (especially when it comes to parameter types)! So make sure to check out also the other parts | ||
| of the docs! | ||
| Parameter types | ||
| --------------- | ||
| A great variety of parameter types are here at your disposal: | ||
| * ``Bool`` | ||
| * ``Integer`` | ||
| * ``String`` | ||
| * ``Number`` | ||
| * ``Vector`` | ||
| * ``Duplet`` | ||
| * ``Triplet`` | ||
| * ``Tuple`` | ||
| * ``PhysicalQuantity`` | ||
| * ``Action`` | ||
| * ``Choice`` | ||
| * ``Group`` | ||
| * ``ComplementaryGroup`` | ||
| * ``SubstitutionGroup`` | ||
| Configuration adaptors | ||
| ---------------------- | ||
| Two adaptor types are provided: | ||
| * ``XMLAdaptor`` for connecting to xml files. | ||
| * ``JSONAdaptor`` for connecting to json files (following some additional conventions). | ||
| Generating configuration files | ||
| ------------------------------ | ||
| Configuration files can of course be created manually however ``anna`` also ships with a ``PyQt`` | ||
| frontend that can be integrated into custom applications. The frontend provides input forms for | ||
| all parameter types as well as for whole parametrized classes together with convenience methods for | ||
| turning the forms' values into configuration adaptor instances which in turn can be dumped to | ||
| files. Both PyQt4 and PyQt5 are supported. See ``anna.frontends.qt``. | ||
| Keywords: configuration framework | ||
| Platform: UNKNOWN | ||
| Classifier: Development Status :: 4 - Beta | ||
| Classifier: License :: OSI Approved :: BSD License | ||
| Classifier: Programming Language :: Python :: 2.7 | ||
| Classifier: Programming Language :: Python :: 3.5 | ||
| Classifier: Programming Language :: Python :: 3.6 | ||
| Classifier: Programming Language :: Python :: 3.7 | ||
| Classifier: Programming Language :: Python :: 3.8 | ||
| Classifier: Programming Language :: Python :: 3.9 | ||
| Classifier: Topic :: Software Development |
| docutils | ||
| numpy | ||
| scipy | ||
| six |
| LICENSE | ||
| MANIFEST.in | ||
| README.rst | ||
| setup.cfg | ||
| setup.py | ||
| anna/LICENSE | ||
| anna/VERSION | ||
| anna/__init__.py | ||
| anna/adaptors.py | ||
| anna/configuration.py | ||
| anna/datatypes.py | ||
| anna/dependencies.py | ||
| anna/exceptions.py | ||
| anna/input.py | ||
| anna/parameters.py | ||
| anna/sweeps.py | ||
| anna/utils.py | ||
| anna.egg-info/PKG-INFO | ||
| anna.egg-info/SOURCES.txt | ||
| anna.egg-info/dependency_links.txt | ||
| anna.egg-info/not-zip-safe | ||
| anna.egg-info/requires.txt | ||
| anna.egg-info/top_level.txt | ||
| anna/frontends/__init__.py | ||
| anna/frontends/qt/__init__.py | ||
| anna/frontends/qt/dialogs.py | ||
| anna/frontends/qt/forms.py | ||
| anna/frontends/qt/parameters.py | ||
| anna/frontends/qt/pyqt45.py | ||
| anna/frontends/qt/sweeps.py | ||
| anna/frontends/qt/utils.py | ||
| anna/frontends/qt/views.py | ||
| anna/frontends/qt/widgets.py | ||
| anna/frontends/qt/icons/info.png | ||
| anna/frontends/qt/icons/parameter_info.png | ||
| anna/unittests/__init__.py | ||
| anna/unittests/adaptors.py | ||
| anna/unittests/configuration.py | ||
| anna/unittests/parameters.py |
| from __future__ import annotations | ||
| from collections import defaultdict, namedtuple | ||
| from dataclasses import dataclass | ||
| from functools import partial | ||
| import inspect | ||
| import math | ||
| from pathlib import Path | ||
| from typing import Any, Callable, List, Sequence, Type, Union | ||
| try: | ||
| from PyQt5.QtWidgets import ( | ||
| QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, QComboBox, QPushButton, QCheckBox, QFileDialog, QDialog, | ||
| QMessageBox, QDialogButtonBox, QScrollArea, QFrame, | ||
| ) | ||
| from PyQt5.QtGui import QDoubleValidator, QIntValidator, QIcon, QFontMetrics | ||
| from PyQt5.QtCore import QLocale, Qt, pyqtSignal | ||
| except ImportError: | ||
| raise RuntimeError('Parameter sweeps only work with PyQt5') | ||
| from anna.adaptors import ConfigurationAdaptor | ||
| from anna.parameters import Parameter, IntegerParameter, NumberParameter, FilepathParameter, PhysicalQuantityParameter | ||
| from anna.sweeps import ( | ||
| FilepathRange, NumberRange, CombinationMethod, ProductCombination, ZipCombination, Generator, Distribution, | ||
| Combinator, VectorRange, Sweep, IntegerRange, Linear, Log10, | ||
| ) | ||
| from .widgets import Folder | ||
| class ParameterRangeForm(QWidget): | ||
| PEER: Type[Parameter] = Parameter | ||
| content_changed = pyqtSignal() | ||
| count_changed = pyqtSignal() | ||
| @property | ||
| def count(self) -> int: | ||
| raise NotImplementedError | ||
| @property | ||
| def why_incomplete_hint(self) -> Union[str, None]: | ||
| raise NotImplementedError | ||
| @property | ||
| def is_complete(self) -> bool: | ||
| return self.why_incomplete_hint is None | ||
| def convert(self) -> Generator: | ||
| raise NotImplementedError | ||
| class NumberRangeForm(ParameterRangeForm): | ||
| PEER = NumberParameter | ||
| RANGE = NumberRange | ||
| DOUBLE_VALIDATOR = QDoubleValidator() | ||
| _locale = QLocale(QLocale.Language.English) | ||
| _locale.setNumberOptions(QLocale.NumberOption.RejectGroupSeparator) | ||
| DOUBLE_VALIDATOR.setLocale(_locale) | ||
| del _locale | ||
| def __init__(self): | ||
| super().__init__() | ||
| self.input_distribution = QComboBox() | ||
| self.input_distribution.addItem('evenly spaced', Distribution.GRID) | ||
| self.input_distribution.addItem('randomly', Distribution.RANDOM) | ||
| self.input_lower_bound = QLineEdit() | ||
| self.input_lower_bound.setValidator(self.DOUBLE_VALIDATOR) | ||
| self.input_lower_bound.setPlaceholderText('lower bound') | ||
| self.input_upper_bound = QLineEdit() | ||
| self.input_upper_bound.setValidator(self.DOUBLE_VALIDATOR) | ||
| self.input_upper_bound.setPlaceholderText('upper bound') | ||
| self.input_count = QLineEdit() | ||
| self.input_count.setValidator(QIntValidator()) | ||
| self.input_count.setPlaceholderText('count') | ||
| self.input_transformation = QComboBox() | ||
| self.input_transformation.addItem('linear', Linear) | ||
| self.input_transformation.addItem('log-linear', Log10) | ||
| self.input_transformation.setToolTip( | ||
| 'linear: distribute data points directly between the specified boundaries\n' | ||
| 'log-linear: take the log10 of the boundaries, then distribute data points and finally convert them back ' | ||
| 'via 10^x' | ||
| ) | ||
| layout = QHBoxLayout() | ||
| layout.addWidget(QLabel('Distribute')) | ||
| layout.addWidget(self.input_distribution) | ||
| layout.addWidget(QLabel('from')) | ||
| layout.addWidget(self.input_lower_bound) | ||
| layout.addWidget(QLabel('to')) | ||
| layout.addWidget(self.input_upper_bound) | ||
| layout.addWidget(QLabel('using')) | ||
| layout.addWidget(self.input_count) | ||
| layout.addWidget(QLabel('points')) | ||
| layout.addWidget(QLabel('(scale:')) | ||
| layout.addWidget(self.input_transformation) | ||
| layout.addWidget(QLabel(')')) | ||
| layout.addStretch(1) | ||
| self.setLayout(layout) | ||
| self.input_lower_bound.textEdited.connect(lambda text: self.content_changed.emit()) | ||
| self.input_upper_bound.textEdited.connect(lambda text: self.content_changed.emit()) | ||
| self.input_count.textEdited.connect(lambda text: self.content_changed.emit()) | ||
| self.input_count.textEdited.connect(lambda text: self.count_changed.emit()) | ||
| @property | ||
| def count(self) -> int: | ||
| try: | ||
| return int(self.input_count.text()) | ||
| except ValueError: | ||
| return _NO_COUNT | ||
| @property | ||
| def why_incomplete_hint(self) -> Union[str, None]: | ||
| if not self.input_lower_bound.text(): | ||
| return 'Lower boundary missing' | ||
| elif not self.input_upper_bound.text(): | ||
| return 'Upper boundary missing' | ||
| elif self.count is _NO_COUNT: | ||
| return 'Count missing' | ||
| else: | ||
| return None | ||
| def convert(self) -> Generator: | ||
| return self.RANGE( | ||
| float(self.input_lower_bound.text()), | ||
| float(self.input_upper_bound.text()), | ||
| int(self.input_count.text()), | ||
| self.input_distribution.currentData(), | ||
| self.input_transformation.currentData(), | ||
| ) | ||
| class IntegerRangeForm(NumberRangeForm): | ||
| PEER = IntegerParameter | ||
| RANGE = IntegerRange | ||
| class FilepathRangeForm(ParameterRangeForm): | ||
| PEER = FilepathParameter | ||
| def __init__(self): | ||
| super().__init__() | ||
| self.input_directory = SelectExistingDirectoryWidget() | ||
| self.input_filter_pattern = QLineEdit() | ||
| self.input_filter_pattern.setPlaceholderText('glob pattern (optional)') | ||
| self.label_how_many_files = QLabel('') | ||
| layout = QHBoxLayout() | ||
| layout.addWidget(QLabel('Scan')) | ||
| layout.addWidget(self.input_directory) | ||
| layout.addWidget(QLabel('using filter')) | ||
| layout.addWidget(self.input_filter_pattern) | ||
| layout.addWidget(self.label_how_many_files) | ||
| layout.addStretch(1) | ||
| self.setLayout(layout) | ||
| self.input_directory.textChanged.connect(lambda text: self.content_changed.emit()) | ||
| self.input_directory.textChanged.connect(lambda text: self.count_changed.emit()) | ||
| self.input_filter_pattern.textChanged.connect(lambda text: self.count_changed.emit()) | ||
| self.count_changed.connect(self._update_how_many_files) | ||
| @property | ||
| def count(self) -> int: | ||
| if self.why_incomplete_hint is None: | ||
| return self.convert().count | ||
| else: | ||
| return _NO_COUNT | ||
| @property | ||
| def why_incomplete_hint(self) -> Union[str, None]: | ||
| if not self.input_directory.text(): | ||
| return 'Directory missing' | ||
| elif not Path(self.input_directory.text()).is_dir(): | ||
| return f'{self.input_directory.text()!s} does not point to an existing directory' | ||
| else: | ||
| return None | ||
| def get_filter_pattern(self) -> str: | ||
| return self.input_filter_pattern.text() or '*' | ||
| def _update_how_many_files(self): | ||
| if (count := self.count) is not _NO_COUNT: | ||
| self.label_how_many_files.setText(f'({count} files)') | ||
| else: | ||
| self.label_how_many_files.setText('') | ||
| def convert(self) -> Generator: | ||
| return FilepathRange(self.input_directory.text(), self.get_filter_pattern()) | ||
| def find_form_by_type(t: Type[Parameter]): | ||
| candidates = (c for c in globals().values() if inspect.isclass(c) and issubclass(c, ParameterRangeForm)) | ||
| if issubclass(t, PhysicalQuantityParameter): | ||
| t = NumberParameter | ||
| for cls in candidates: | ||
| if cls.PEER == t: | ||
| return cls | ||
| raise LookupError(f'No form for parameter type {t}') | ||
| class ParameterRangeWidget(QWidget): | ||
| content_changed = pyqtSignal() | ||
| count_changed = pyqtSignal() | ||
| FORMATTER: dict[Type, Callable[[Any], str]] = { | ||
| float: lambda x: str(x) if x == 0 or 1e-3 <= abs(x) < 1e4 else f'{x:.3e}', | ||
| } | ||
| def __init__(self, name: str, value: Any, form: ParameterRangeForm, *, path: str = None): | ||
| super().__init__() | ||
| self.name = name | ||
| self.form = form | ||
| self.path = path | ||
| layout = QHBoxLayout() | ||
| layout.addWidget(QLabel(f'<b>{name}</b> (value: {self.value_to_string(value)})')) | ||
| layout.addWidget(form) | ||
| self.setLayout(layout) | ||
| self.form.content_changed.connect(lambda: self.content_changed.emit()) | ||
| self.form.count_changed.connect(lambda: self.count_changed.emit()) | ||
| @property | ||
| def count(self) -> int: | ||
| return self.form.count | ||
| @property | ||
| def is_complete(self) -> bool: | ||
| return self.form.is_complete | ||
| @property | ||
| def why_incomplete_hint(self) -> Union[str, None]: | ||
| if (hint := self.form.why_incomplete_hint) is not None: | ||
| return f'{self.name}: {hint}' | ||
| return None | ||
| def convert(self) -> Generator: | ||
| return self.form.convert() | ||
| @classmethod | ||
| def value_to_string(cls, value): | ||
| for tp, formatter in cls.FORMATTER.items(): | ||
| if isinstance(value, tp): | ||
| return formatter(value) | ||
| return str(value) | ||
| AParam = namedtuple('AParam', 'active widget') | ||
| class CombinationWidget(QWidget): | ||
| content_changed = pyqtSignal() | ||
| count_changed = pyqtSignal() | ||
| def __init__( | ||
| self, | ||
| widgets: Union[list[ParameterRangeWidget | CombinationWidget], dict[str, Sequence[ParameterRangeWidget | CombinationWidget]]], | ||
| *, | ||
| title: str, | ||
| path: str = None, | ||
| ): | ||
| super().__init__() | ||
| self.name = title | ||
| self.path = path | ||
| self.parameters: List[AParam] = [] | ||
| self.input_combination_method = QComboBox() | ||
| self.input_combination_method.addItem('product', ProductCombination) | ||
| self.input_combination_method.addItem('zip', ZipCombination) | ||
| self.input_combination_method.currentIndexChanged.connect(lambda index: self.count_changed.emit()) | ||
| self.input_combination_method.setToolTip( | ||
| 'product: for each data point of a parameter, consider all possible combinations of all other parameters ' | ||
| '(n = n1*n2*n3*...)' | ||
| '\n' | ||
| 'zip: combine the values of all parameters by building pairs that correspond to the first value of each ' | ||
| 'parameter, the second value, ... (n = min(n1, n2, n3, ...))' | ||
| ) | ||
| title_layout = QHBoxLayout() | ||
| title_layout.addWidget(QLabel(f'<b>{title}</b>')) | ||
| title_layout.addWidget(QLabel('combine as')) | ||
| title_layout.addWidget(self.input_combination_method) | ||
| title_layout.addStretch(1) | ||
| layout = QVBoxLayout() | ||
| layout.addLayout(title_layout) | ||
| if isinstance(widgets, list): | ||
| layout.addLayout(self._create_flat_layout(widgets)) | ||
| elif isinstance(widgets, dict): | ||
| layout.addLayout(self._create_nested_layout(widgets)) | ||
| else: | ||
| raise TypeError(f'Invalid container type for widgets: {type(widgets)}') | ||
| layout.addStretch(1) | ||
| self.setLayout(layout) | ||
| self.setStyleSheet('CombinationWidget { border: 1px solid black; border-radius: 5px; }') | ||
| self.setAttribute(Qt.WA_StyledBackground, True) | ||
| def _create_flat_layout(self, widgets: list[ParameterRangeWidget | CombinationWidget]): | ||
| class NonFolder(QWidget): | ||
| def __init__(self, *, title): | ||
| super().__init__() | ||
| self._title = title | ||
| def notify_parameter_activated(self, param, active): | ||
| pass | ||
| def setContentLayout(self, layout): | ||
| self.setLayout(layout) | ||
| # The dict key is not important, it will be ignored by NonFolder. | ||
| return self._create_layout(dict(_=widgets), NonFolder) | ||
| def _create_nested_layout(self, widgets: dict[str, Sequence[ParameterRangeWidget | CombinationWidget]]): | ||
| class ColorFolder(Folder): | ||
| COLOR_INACTIVE = '000000' | ||
| COLOR_ACTIVE = '009925' | ||
| def __init__(self, *, title): | ||
| super().__init__(title=title) | ||
| self._activate_count = 0 | ||
| # noinspection PyUnusedLocal | ||
| def notify_parameter_activated(self, param, active): | ||
| self._activate_count += (2*active - 1) | ||
| if self._activate_count > 0: | ||
| style_sheet = f'QToolButton {{ border: none; color: #{self.COLOR_ACTIVE}}}' | ||
| elif self._activate_count == 0: | ||
| style_sheet = f'QToolButton {{ border: none; color: #{self.COLOR_INACTIVE}}}' | ||
| else: | ||
| assert False | ||
| self.toggleButton.setStyleSheet(style_sheet) | ||
| return self._create_layout(widgets, ColorFolder) | ||
| def _create_layout( | ||
| self, | ||
| widget_groups: dict[str, Sequence[ParameterRangeWidget | CombinationWidget]], | ||
| group_container_cls, | ||
| ): | ||
| def _generate_checkbox_slot(param: QWidget, group_): | ||
| def _slot(state): | ||
| active = state == Qt.CheckState.Checked | ||
| param.setEnabled(active) | ||
| group_.notify_parameter_activated(param, active) | ||
| return _slot | ||
| layout = QVBoxLayout() | ||
| for group_title, widgets in widget_groups.items(): | ||
| group = group_container_cls(title=group_title) | ||
| group_layout = QVBoxLayout() | ||
| for parameter in widgets: | ||
| parameter.setEnabled(False) | ||
| parameter.content_changed.connect(lambda: self.content_changed.emit()) | ||
| parameter.count_changed.connect(lambda: self.count_changed.emit()) | ||
| check_box = QCheckBox() | ||
| check_box.stateChanged.connect(_generate_checkbox_slot(parameter, group)) | ||
| check_box.stateChanged.connect(lambda: self.content_changed.emit()) | ||
| check_box.stateChanged.connect(lambda: self.count_changed.emit()) | ||
| parameter_layout = QHBoxLayout() | ||
| parameter_layout.addWidget(check_box) | ||
| parameter_layout.addWidget(parameter) | ||
| group_layout.addLayout(parameter_layout) | ||
| self.parameters.append(AParam(check_box, parameter)) | ||
| group.setContentLayout(group_layout) | ||
| layout.addWidget(group) | ||
| return layout | ||
| @property | ||
| def active_widgets(self) -> List[ParameterRangeWidget|CombinationWidget]: | ||
| return [p.widget for p in self.parameters if p.active.checkState() == Qt.CheckState.Checked] | ||
| @property | ||
| def count(self) -> int: | ||
| if self.active_widgets: | ||
| combination_method = self.input_combination_method.currentData() | ||
| return combination_method(self.active_widgets).count | ||
| else: | ||
| return _NO_COUNT | ||
| @property | ||
| def is_complete(self) -> bool: | ||
| return all(w.is_complete for w in self.active_widgets) | ||
| @property | ||
| def why_incomplete_hint(self) -> Union[str, None]: | ||
| if (hint := next((w.why_incomplete_hint for w in self.active_widgets if not w.is_complete), None)) is not None: | ||
| return f'{self.name}/{hint}' | ||
| return None | ||
| def convert(self) -> Combinator: | ||
| method = self.input_combination_method.currentData() | ||
| if method is ZipCombination and len({w.count for w in self.active_widgets}) > 1: | ||
| QMessageBox.warning( | ||
| self, | ||
| f'{self.path}/{self.name}' if self.path else self.name, | ||
| 'ZIP combination will discard excess data points if not all parameters have the same count.', | ||
| ) | ||
| return Combinator([w.convert() for w in self.active_widgets], method=method) | ||
| class VectorCombinationWidget(CombinationWidget): | ||
| def __init__( | ||
| self, | ||
| names: Sequence[str], | ||
| values: Sequence[Any], | ||
| forms: Sequence[ParameterRangeForm], | ||
| *, | ||
| default_values: Sequence[Any], | ||
| title: str, | ||
| path: str = None, | ||
| ): | ||
| assert len(names) == len(values) == len(forms) | ||
| widgets = [ParameterRangeWidget(name, value, form) for name, value, form in zip(names, values, forms)] | ||
| super().__init__(widgets, title=title, path=path) | ||
| self.default_values = default_values | ||
| assert len(self.parameters) == len(self.default_values) | ||
| def convert(self) -> Combinator: | ||
| super().convert() # Display warnings, if applicable. | ||
| method = self.input_combination_method.currentData() | ||
| if method is ProductCombination: | ||
| default_generator = partial(SequenceGenerator.from_value, count=1) | ||
| elif method is ZipCombination: | ||
| default_generator = partial(SequenceGenerator.from_value, count=self.count) | ||
| else: | ||
| assert False | ||
| active_widgets = self.active_widgets | ||
| return Combinator( | ||
| [p.widget.convert() if p.widget in active_widgets else default_generator(v) | ||
| for p, v in zip(self.parameters, self.default_values)], | ||
| method=method, | ||
| ) | ||
| def find_widget_for_parameter(parameter: Parameter, value: Any, *, path: str = None): | ||
| p_type = type(parameter) | ||
| is_vector_parameter = p_type.__name__.endswith(('Duplet', 'Triplet', 'Vector')) | ||
| if is_vector_parameter: | ||
| # noinspection PyProtectedMember | ||
| p_type = parameter._element_type | ||
| if issubclass(p_type, PhysicalQuantityParameter): | ||
| value_formatter = lambda v: f'{ParameterRangeWidget.value_to_string(v)} {parameter.unit!s}' | ||
| else: | ||
| value_formatter = ParameterRangeWidget.value_to_string | ||
| if is_vector_parameter: | ||
| f_type = find_form_by_type(p_type) | ||
| widget = VectorCombinationWidget( | ||
| names=[f'[{i}]' for i in range(len(value))], | ||
| values=[value_formatter(v) for v in value], | ||
| forms=[f_type() for _ in value], | ||
| default_values=value, | ||
| title=parameter.name, | ||
| path=path, | ||
| ) | ||
| else: | ||
| f_type = find_form_by_type(p_type) | ||
| widget = ParameterRangeWidget( | ||
| name=parameter.name, | ||
| value=value_formatter(value), | ||
| form=f_type(), | ||
| path=path, | ||
| ) | ||
| return widget | ||
| class SelectExistingDirectoryWidget(QWidget): | ||
| PEER = FilepathParameter | ||
| INPUT_FIELD_PADDING = 30 | ||
| def __init__(self): | ||
| super().__init__() | ||
| self.input = QLineEdit() | ||
| self.input.setPlaceholderText('directory') | ||
| self.input.textChanged.connect(self._resize_line_edit) | ||
| self._input_min_width = self.input.width() | ||
| self.input.setFixedWidth(self._input_min_width) | ||
| button = QPushButton(QIcon.fromTheme('folder-open'), '') | ||
| button.clicked.connect(lambda: self.input.setText( | ||
| QFileDialog.getExistingDirectory(caption='Choose an existing directory'))) | ||
| layout = QHBoxLayout() | ||
| layout.addWidget(self.input) | ||
| layout.addWidget(button) | ||
| self.setLayout(layout) | ||
| @property | ||
| def textChanged(self): | ||
| return self.input.textChanged | ||
| def text(self): | ||
| return self.input.text() | ||
| def _resize_line_edit(self, text): | ||
| font_metric = QFontMetrics(self.input.font()) | ||
| self.input.setFixedWidth(max(font_metric.width(text) + self.INPUT_FIELD_PADDING, self._input_min_width)) | ||
| self.adjustSize() | ||
| class SequenceGenerator(Generator): | ||
| def __init__(self, sequence: Sequence): | ||
| self.sequence = sequence | ||
| @property | ||
| def count(self) -> int: | ||
| return len(self.sequence) | ||
| def generate(self) -> Sequence: | ||
| return self.sequence | ||
| @classmethod | ||
| def from_value(cls, value: Any, *, count: int): | ||
| return cls([value] * count) | ||
| class _NAInt(int): | ||
| def _op(self, other): | ||
| if isinstance(other, int): | ||
| return self | ||
| return NotImplemented | ||
| __add__ = __radd__ = __mul__ = __rmul__ = _op | ||
| _NO_COUNT = _NAInt() | ||
| class SweepWidget(QWidget): | ||
| WIDGET_TITLE = 'Parameter sweep' | ||
| generate_callbacks: List[Callable[[Sweep], None]] | ||
| def __init__( | ||
| self, | ||
| widget_groups: dict[str, Sequence[ParameterRangeWidget | CombinationWidget]], | ||
| ): | ||
| super().__init__() | ||
| self.combination_widget = CombinationWidget(widget_groups, title=self.WIDGET_TITLE) | ||
| self.generate_callbacks = [] | ||
| self.button_generate = QPushButton('Generate') | ||
| self.button_generate.setEnabled(False) | ||
| self.button_generate.clicked.connect(self.generate) | ||
| button_layout = QHBoxLayout() | ||
| button_layout.addStretch(1) | ||
| button_layout.addWidget(self.button_generate) | ||
| button_layout.addStretch(1) | ||
| layout = QVBoxLayout() | ||
| scroll_area = QScrollArea() | ||
| scroll_area.setWidget(self.combination_widget) | ||
| scroll_area.setWidgetResizable(True) | ||
| layout.addWidget(scroll_area) | ||
| layout.addLayout(button_layout) | ||
| self.setLayout(layout) | ||
| self.combination_widget.content_changed.connect(self._content_changed_slot) | ||
| self.combination_widget.count_changed.connect(self._count_changed_slot) | ||
| @property | ||
| def is_complete(self) -> bool: | ||
| return self.combination_widget.count is not _NO_COUNT and self.combination_widget.is_complete | ||
| @property | ||
| def why_incomplete_hint(self) -> Union[str, None]: | ||
| return self.combination_widget.why_incomplete_hint | ||
| def _count_changed_slot(self): | ||
| if (count := self.combination_widget.count) is not _NO_COUNT: | ||
| self.button_generate.setText(f'Generate (n = {count})') | ||
| else: | ||
| self.button_generate.setText('Generate') | ||
| def _content_changed_slot(self): | ||
| self.button_generate.setEnabled(self.is_complete) | ||
| self.button_generate.setToolTip(self.why_incomplete_hint or '') | ||
| def generate(self): | ||
| names = [(f'{w.path}/{w.name}' if w.path else w.name) for w in self.combination_widget.active_widgets] | ||
| sweep = Sweep(names, self.combination_widget.convert()) | ||
| for callback in self.generate_callbacks: | ||
| callback(sweep) | ||
| @classmethod | ||
| def from_configuration(cls, adaptor: ConfigurationAdaptor, parameters: dict[str, list[Parameter]]) -> SweepWidget: | ||
| widgets = defaultdict(list) | ||
| for config_path, params in parameters.items(): | ||
| for param in params: | ||
| widgets[config_path].append(find_widget_for_parameter( | ||
| param, | ||
| param.load_from_configuration(adaptor, config_path), | ||
| path=config_path, | ||
| )) | ||
| return cls(widgets) |
+245
| from __future__ import annotations | ||
| import itertools as it | ||
| import math | ||
| from pathlib import Path | ||
| import re | ||
| from typing import Any, Callable, Sequence, Type, Union | ||
| import numpy as np | ||
| import json | ||
| import pandas as pd | ||
| from anna.adaptors import XMLAdaptor | ||
| from anna.exceptions import InvalidPathError | ||
| class Generator: | ||
| count: int | ||
| def generate(self) -> Sequence: raise NotImplementedError | ||
| DistributionType = Callable[[float, float, int], Sequence[float]] | ||
| class Distribution: | ||
| GRID = np.linspace | ||
| RANDOM = np.random.default_rng().uniform | ||
| class Transformation: | ||
| def apply(self, x): raise NotImplementedError | ||
| def inverse(self, x): raise NotImplementedError | ||
| class Linear(Transformation): | ||
| def apply(self, x): | ||
| return x | ||
| def inverse(self, x): | ||
| return x | ||
| class Log10(Transformation): | ||
| def apply(self, x): | ||
| return np.log10(x) | ||
| def inverse(self, x): | ||
| return 10 ** x | ||
| class NumberRange(Generator): | ||
| def __init__(self, lower: float, upper: float, count: int, method: DistributionType, scale: Type[Transformation]): | ||
| self.scale = scale() | ||
| self.lower = lower | ||
| self.upper = upper | ||
| self.count = count | ||
| self.method = method | ||
| def generate(self) -> Sequence[float]: | ||
| return self.scale.inverse(self.method(self.scale.apply(self.lower), self.scale.apply(self.upper), self.count)) | ||
| class IntegerRange(NumberRange): | ||
| def generate(self) -> Sequence[int]: | ||
| return np.rint(super().generate()).astype(np.int64) | ||
| class FilepathRange(Generator): | ||
| def __init__(self, path: Path | str, pattern: str): | ||
| self.path = Path(path) | ||
| self.pattern = pattern | ||
| def generate(self) -> Sequence[Path]: | ||
| if not self.path.is_dir(): | ||
| raise ValueError(f'{self.path.resolve()!s} does not point to an existing directory') | ||
| return list(self.path.glob(self.pattern)) | ||
| @property | ||
| def count(self) -> int: | ||
| return len(self.generate()) | ||
| class CombinationMethod: | ||
| def __init__(self, parameters: Sequence[Generator]): | ||
| self.parameters = parameters | ||
| def generate(self) -> Sequence[Sequence]: | ||
| raise NotImplementedError | ||
| @property | ||
| def count(self) -> int: | ||
| raise NotImplementedError | ||
| class ProductCombination(CombinationMethod): | ||
| def generate(self) -> list[tuple]: | ||
| return list(it.product(*(p.generate() for p in self.parameters))) | ||
| @property | ||
| def count(self) -> int: | ||
| return math.prod(p.count for p in self.parameters) | ||
| class ZipCombination(CombinationMethod): | ||
| def generate(self) -> list[tuple]: | ||
| return list(zip(*(p.generate() for p in self.parameters))) | ||
| @property | ||
| def count(self) -> int: | ||
| return min(p.count for p in self.parameters) | ||
| CombinationMethodType = Type[CombinationMethod] | ||
| class Combinator(Generator): | ||
| def __init__(self, parameters: Sequence[Generator], method: CombinationMethodType): | ||
| self.parameters = parameters | ||
| self.method = method | ||
| def generate(self) -> np.ndarray: | ||
| return np.asarray(self.method(self.parameters).generate(), dtype=object) | ||
| class VectorRange(Combinator): | ||
| def __init__( | ||
| self, | ||
| lowers: Sequence[float], | ||
| uppers: Sequence[float], | ||
| counts: Sequence[int], | ||
| methods: Sequence[DistributionType], | ||
| scales: Sequence[Type[Transformation]], | ||
| *, | ||
| method: CombinationMethodType | ||
| ): | ||
| assert len(lowers) == len(uppers) == len(counts) == len(methods) == len(scales) | ||
| super().__init__( | ||
| [NumberRange(l, u, c, m, s) | ||
| for l, u, c, m, s in zip(lowers, uppers, counts, methods, scales)], | ||
| method=method | ||
| ) | ||
| class Sweep: | ||
| def __init__(self, names: Sequence[str], combinator: Combinator): | ||
| self.dataframe = pd.DataFrame.from_records( | ||
| data=combinator.generate(), | ||
| columns=names, | ||
| ) | ||
| def __len__(self): | ||
| return len(self.dataframe) | ||
| def to_dataframe(self) -> pd.DataFrame: | ||
| return self.dataframe | ||
| def to_json(self, json_path=None) -> Union[str, None]: | ||
| return self.dataframe.to_json( | ||
| path_or_buf=json_path, | ||
| orient='index', | ||
| indent=4, | ||
| default_handler=lambda path: str(path.resolve()), | ||
| ) | ||
| class Generate: | ||
| CONFIG_DIRECTORY = 'configurations' | ||
| def __init__( | ||
| self, | ||
| sweep_instance: Sweep, | ||
| *, | ||
| seed_path: Path, | ||
| folder_path: Path, | ||
| config_prefix: str, | ||
| meta: dict[str, dict[str, Any]], | ||
| constants: dict[str, Any] | ||
| ): | ||
| self.seed_path = seed_path | ||
| self.folder_path = folder_path | ||
| self.config_prefix = config_prefix | ||
| self.sweep_instance = sweep_instance | ||
| self.json_file_dict = json.loads(self.sweep_instance.to_json()) | ||
| self.meta = meta | ||
| self.constants = constants | ||
| self.xml_seed = XMLAdaptor(str(self.seed_path.resolve())) | ||
| self.number_of_files = len(self.sweep_instance.to_dataframe()) | ||
| self.generated_names = self.names() | ||
| def all(self): | ||
| self.xml() | ||
| self.names_to_txt() | ||
| self.csv() | ||
| def xml(self): | ||
| path_config = self.folder_path.joinpath(self.CONFIG_DIRECTORY) | ||
| # Create directories, error if directories already exist to prevent data loss. | ||
| path_config.mkdir() | ||
| # Create JSON file | ||
| self.sweep_instance.to_json(str(self.folder_path.joinpath('sweep').with_suffix('.json').resolve())) | ||
| # Functionality that adds parameter to XML file if not contained in seed XML file | ||
| for key in self.sweep_instance.to_dataframe().columns: | ||
| try: | ||
| self.xml_seed.get_text(key) | ||
| except InvalidPathError: | ||
| self.xml_seed.insert_element(key, 'placeholder', **self.meta.get(key, {})) | ||
| # Create the configuration files for each parameter combination | ||
| for index, (combination_id, combination_info) in enumerate(self.json_file_dict.items()): | ||
| for key, value in combination_info.items(): | ||
| self.xml_seed.update_text(key, str(value)) | ||
| # Dump into new XML file | ||
| total_config_path = path_config.joinpath(self.generated_names[index]).resolve() | ||
| self.xml_seed.dump_to_file(str(total_config_path)) | ||
| # The following is a workaround for https://gitlab.com/Dominik1123/Anna/-/issues/23 | ||
| # `XMLAdaptor.dump_to_file` adds additional empty lines to the output XML file which are not present in the | ||
| # seed file. This makes it more difficult to compare the two versions, hence removing empty lines here. | ||
| total_config_path.write_text(re.sub( | ||
| '^[ \t]*\r?\n', | ||
| '', | ||
| total_config_path.read_text(), | ||
| flags=re.M, | ||
| )) | ||
| def names(self): | ||
| config_names = [] | ||
| num_zeros = int(np.ceil(np.log10(len(self.sweep_instance.to_dataframe())))) | ||
| for ii in range(self.number_of_files): | ||
| config_names.append(self.config_prefix + str(ii).zfill(num_zeros) + '.xml') | ||
| return config_names | ||
| def names_to_txt(self): | ||
| with open(self.folder_path.joinpath('names_to_txt').with_suffix('.txt').resolve(), 'w') as f: | ||
| for x in self.generated_names: | ||
| f.write(f'{Path(self.CONFIG_DIRECTORY).joinpath(x)!s}\n') | ||
| def csv(self): | ||
| dataframe = self.sweep_instance.to_dataframe() | ||
| # Need to convert to `[v]*len(...)` since the constant values `v` might be lists themselves (VectorParameter). | ||
| dataframe = dataframe.assign(**{k: [v]*len(self.sweep_instance) for k, v in self.constants.items()}) | ||
| dataframe.insert(0, 'Configuration filename', self.generated_names) | ||
| dataframe = dataframe.applymap(np.asarray) | ||
| path_csv_final = self.folder_path.joinpath('data').with_suffix('.csv') | ||
| dataframe.to_csv(str(path_csv_final.resolve())) | ||
| if __name__ == "__main__": | ||
| pass |
+23
| Copyright 2017 Dominik Vilsmeier | ||
| Redistribution and use in source and binary forms, with or without modification, are permitted | ||
| provided that the following conditions are met: | ||
| 1. Redistributions of source code must retain the above copyright notice, this list of conditions | ||
| and the following disclaimer. | ||
| 2. Redistributions in binary form must reproduce the above copyright notice, this list of | ||
| conditions and the following disclaimer in the documentation and/or other materials provided | ||
| with the distribution. | ||
| 3. Neither the name of the copyright holder nor the names of its contributors may be used to | ||
| endorse or promote products derived from this software without specific prior written permission. | ||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR | ||
| IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND | ||
| FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR | ||
| CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||
| DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
| DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER | ||
| IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT | ||
| OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| include anna/LICENSE | ||
| include anna/VERSION | ||
| include anna/frontends/qt/icons/info.png | ||
| include anna/frontends/qt/icons/parameter_info.png |
+315
| Metadata-Version: 1.1 | ||
| Name: anna | ||
| Version: 0.4 | ||
| Summary: A Neat configuratioN Auxiliary | ||
| Home-page: https://gitlab.com/Dominik1123/Anna | ||
| Author: Dominik Vilsmeier | ||
| Author-email: dominik.vilsmeier1123@gmail.com | ||
| License: BSD-3-Clause | ||
| Description: Anna - A Neat configuratioN Auxiliary | ||
| ===================================== | ||
| Anna helps you configure your application by building the bridge between the components of | ||
| your application and external configuration sources. It allows you to keep your code short and | ||
| flexible yet explicit when it comes to configuration - the necessary tinkering is performed by | ||
| the framework. | ||
| Anna contains lots of "in-place" documentation aka doc strings so make sure you check out those | ||
| too ("``help`` yourself")! | ||
| 80 seconds to Anna | ||
| ------------------ | ||
| Anna is all about *parameters* and *configuration sources*. You declare parameters as part of | ||
| your application (on a class for example) and specify their values in a configuration source. | ||
| All you're left to do with then is to point your application to the configuration source and | ||
| let the framework do its job. | ||
| An example is worth a thousand words | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| Say we want to build an application that deals with vehicles. I'm into cars so the first thing | ||
| I'll do is make sure we get one of those:: | ||
| >>> class Car: | ||
| ... def __init__(self, brand, model): | ||
| ... self._brand = brand | ||
| ... self._model = model | ||
| >>> | ||
| >>> your_car = Car('Your favorite brand', 'The hottest model') | ||
| Great! We let the user specify the car's ``brand`` and ``model`` and return him a brand new car! | ||
| Now we're using ``anna`` for declaring the parameters:: | ||
| >>> from anna import Configurable, parametrize, String, JSONAdaptor | ||
| >>> | ||
| >>> @parametrize( | ||
| ... String('Brand'), | ||
| ... String('Model') | ||
| ... ) | ||
| ... class Car(Configurable): | ||
| ... def __init__(self, config): | ||
| ... super(Car, self).__init__(config) | ||
| >>> | ||
| >>> your_car = Car(JSONAdaptor('the_file_where_you_specified_your_favorite_car.json')) | ||
| The corresponding json file would look like this:: | ||
| { | ||
| "Car/Parameters/Brand": "Your favorite brand", | ||
| "Car/Parameters/Model": "The hottest model", | ||
| } | ||
| It's a bit more to type but this comes at a few advantages: | ||
| * We can specify the type of the parameter and ``anna`` will handle the necessary conversions | ||
| for us; ``anna`` ships with plenty of parameter types so there's much more to it than just | ||
| strings! | ||
| * If we change your mind later on and want to add another parameter, say for example the color | ||
| of the car, it's as easy as declaring a new parameter ``String('Color')`` and setting it as | ||
| a class attribute; all the user needs to do is to specify the corresponding value in | ||
| the configuration source. Note that there's no need to change any interfaces/signatures or | ||
| other intermediate components which carry the user input to the receiving class; all it expects | ||
| is a configuration adaptor which points to the configuration source. | ||
| * The configuration source can host parameters for more than only one component, meaning again | ||
| that we don't need to modify intermediate parts when adding new components to our application; | ||
| all we need to do is provide the configuration adaptor. | ||
| Five minutes hands-on | ||
| --------------------- | ||
| The 80 seconds intro piqued your curiosity? Great! So let's move on! For the following | ||
| considerations we'll pick up the example from above and elaborate on it more thoroughly. | ||
| Let's start with a quick Q/A session | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| **So what happened when using the decorator ``parametrize``?** It received a number of parameters | ||
| as arguments which it set as attributes on the receiving class. Field names are deduced from | ||
| the parameters names applying CamelCase to _snake_case_with_leading_underscore conversion. | ||
| That is ``String('Brand')`` is set as ``Car._brand``. | ||
| **All right, but how did the instance receive its values then?** Note that ``Car`` inherits from | ||
| ``Configurable`` and ``Configurable.__init__`` is where the actual instance configuration happens. | ||
| We provided it a configuration adaptor which points to the configuration source (in this case | ||
| a local file) and the specific values were extracted from there. Values are set on the instance | ||
| using the parameter's field name, that is ``String('Brand')`` will make an instance receive | ||
| the corresponding value at ``your_car._brand`` (``Car._brand`` is still the parameter instance). | ||
| **Okay, but how did the framework know where to find the values in the configuration source?** | ||
| Well there's a bit more going on during the call to ``parametrize`` than is written above. | ||
| In addition to setting the parameters on the class it also deduces a configuration path for | ||
| each parameter which specifies where to find the corresponding value in the source. The path | ||
| consists of a base path and the parameter's name: "<base-path>/<name>" (slashes are used | ||
| to delimit path elements). ``parametrize`` tries to get this base path from the receiving class | ||
| looking up the attribute ``CONFIG_PATH``. If it has no such attribute or if it's ``None`` then | ||
| the base path defaults to "<class-name>/Parameters". However in our example - although we didn't | ||
| set the config path explicitly - it was already there because ``Configurable`` uses a custom | ||
| metaclass which adds the class attribute ``CONFIG_PATH`` if it's missing or ``None`` using | ||
| the same default as above. So if you want to specify a custom path within the source you can do so | ||
| by specifying the class attribute ``CONFIG_PATH``. | ||
| **_snake_case_with_leading_underscore, not too bad but can I choose custom field names for the parameters too?** | ||
| Yes, besides providing a number of parameters as arguments to ``parametrize`` we have the option | ||
| to supply it a number of keyword arguments as well which represent field_name / parameter pairs; | ||
| the key is the field name and the value is the parameter: ``brand_name=String('Brand')``. | ||
| **Now that we declared all those parameters how does the user know what to specify?** | ||
| ``anna`` provides a decorator ``document_parameters`` which will add all declared parameters to | ||
| the component's doc string under a new section. Another option for the user is to retrieve | ||
| the declared parameters via ``get_parameters`` (which is inherited from ``Configurable``) and | ||
| print their string representations which contain comprehensive information:: | ||
| >>> for parameter in Car.get_parameters(): | ||
| ... print(parameter) | ||
| Of course documenting the parameters manually is also an option. | ||
| Alright so let's get to the code | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| :: | ||
| >>> from anna import Configurable, parametrize, String, JSONAdaptor | ||
| >>> | ||
| >>> @parametrize( | ||
| ... String('Model'), | ||
| ... brand_name=String('Brand') | ||
| ... ) | ||
| ... class Car(Configurable): | ||
| ... CONFIG_PATH = 'Car' | ||
| ... def __init__(self, config): | ||
| ... super(Car, self).__init__(config) | ||
| Let's first see what information we can get about the parameters:: | ||
| >>> for parameter in Car.get_parameters(): | ||
| ... print(parameter) | ||
| ... | ||
| { | ||
| "optional": false, | ||
| "type": "StringParameter", | ||
| "name": "Model", | ||
| "path": "Car" | ||
| } | ||
| { | ||
| "optional": false, | ||
| "type": "StringParameter", | ||
| "name": "Brand", | ||
| "path": "Car" | ||
| } | ||
| Note that it prints ``"StringParameter"`` because that's the parameter's actual class, | ||
| ``String`` is just a shorthand. Let's see what we can get from the doc string:: | ||
| >>> print(Car.__doc__) | ||
| None | ||
| >>> from anna import document_parameters | ||
| >>> Car = document_parameters(Car) | ||
| >>> print(Car.__doc__) | ||
| Declared parameters | ||
| ------------------- | ||
| (configuration path: Car) | ||
| Brand : String | ||
| Model : String | ||
| Now that we know what we need to specify let's get us a car! The ``JSONAdaptor`` can also be | ||
| initialized with a ``dict`` as root element, so we're just creating our configuration on the fly:: | ||
| >>> back_to_the_future = JSONAdaptor(root={ | ||
| ... 'Car/Brand': 'DeLorean', | ||
| ... 'Car/Model': 'DMC-12', | ||
| ... }) | ||
| >>> doc_browns_car = Car(back_to_the_future) | ||
| >>> doc_browns_car.brand_name # Access via our custom field name. | ||
| 'DeLorean' | ||
| >>> doc_browns_car._model # Access via the automatically chosen field name. | ||
| 'DMC-12' | ||
| Creating another car is as easy as providing another configuration source:: | ||
| >>> mr_bonds_car = Car(JSONAdaptor(root={ | ||
| ... 'Car/Brand': 'Aston Martin', | ||
| ... 'Car/Model': 'DB5', | ||
| ... })) | ||
| Let's assume we want more information about the brand than just its name. We have nicely stored | ||
| all information in a database:: | ||
| >>> database = { | ||
| ... 'DeLorean': { | ||
| ... 'name': 'DeLorean', | ||
| ... 'founded in': 1975, | ||
| ... 'founded by': 'John DeLorean', | ||
| ... }, | ||
| ... 'Aston Martin': { | ||
| ... 'name': 'Aston Martin', | ||
| ... 'founded in': 1913, | ||
| ... 'founded by': 'Lionel Martin, Robert Bamford', | ||
| ... }} | ||
| We also have a database access function which we can use to load stuff from the database:: | ||
| >>> def load_from_database(key): | ||
| ... return database[key] | ||
| To load this database information instead of just the brand's name we only have to modify | ||
| the ``Car`` class to declare a new parameter: ``ActionParameter`` (or ``Action``). | ||
| An ``ActionParameter`` wraps another parameter and let's us specify an action which is applied to | ||
| the parameter's value when it's loaded. For our case that is:: | ||
| >>> from anna import ActionParameter | ||
| >>> Car.brand = ActionParameter(String('Brand'), load_from_database) | ||
| >>> doc_browns_car = Car(back_to_the_future) | ||
| >>> doc_browns_car.brand | ||
| {'founded by': 'John DeLorean', 'name': 'DeLorean', 'founded in': 1975} | ||
| >>> doc_browns_car.brand_name | ||
| 'DeLorean' | ||
| Note that we didn't need to provide a new configuration source as the new ``brand`` parameter is | ||
| based on the brand name which is already present. | ||
| Say we also want to obtain the year in which the model was first produced and we have a function | ||
| for exactly that purpose however it requires the brand name and model name as one string:: | ||
| >>> def first_produced_in(brand_and_model): | ||
| ... return {'DeLorean DMC-12': 1981, 'Aston Martin DB5': 1963}[brand_and_model] | ||
| That's not a problem because an ``ActionParameter`` type lets us combine multiple parameters:: | ||
| >>> Car.first_produced_in = ActionParameter( | ||
| ... String('Brand'), | ||
| ... lambda brand, model: first_produced_in('%s %s' % (brand, model)), | ||
| ... depends_on=('Model',)) | ||
| Other existing parameters, specified either by name of by reference via the keyword argument | ||
| ``depends_on``, are passed as additional arguments to the given action. | ||
| In the above example we declared parameters on a class using ``parametrize`` but you could as well | ||
| use parameter instances independently and load their values via ``load_from_configuration`` which | ||
| expects a configuration adaptor as well as a configuration path which localizes the parameter's | ||
| value. You also have the option to provide a specification directly via | ||
| ``load_from_representation``. This functions expects the specification as a unicode string and | ||
| additional (meta) data as a ``dict`` (a unit for ``PhysicalQuantities`` for example). | ||
| This introduction was meant to demonstrate the basic principles but there's much more to ``anna`` | ||
| (especially when it comes to parameter types)! So make sure to check out also the other parts | ||
| of the docs! | ||
| Parameter types | ||
| --------------- | ||
| A great variety of parameter types are here at your disposal: | ||
| * ``Bool`` | ||
| * ``Integer`` | ||
| * ``String`` | ||
| * ``Number`` | ||
| * ``Vector`` | ||
| * ``Duplet`` | ||
| * ``Triplet`` | ||
| * ``Tuple`` | ||
| * ``PhysicalQuantity`` | ||
| * ``Action`` | ||
| * ``Choice`` | ||
| * ``Group`` | ||
| * ``ComplementaryGroup`` | ||
| * ``SubstitutionGroup`` | ||
| Configuration adaptors | ||
| ---------------------- | ||
| Two adaptor types are provided: | ||
| * ``XMLAdaptor`` for connecting to xml files. | ||
| * ``JSONAdaptor`` for connecting to json files (following some additional conventions). | ||
| Generating configuration files | ||
| ------------------------------ | ||
| Configuration files can of course be created manually however ``anna`` also ships with a ``PyQt`` | ||
| frontend that can be integrated into custom applications. The frontend provides input forms for | ||
| all parameter types as well as for whole parametrized classes together with convenience methods for | ||
| turning the forms' values into configuration adaptor instances which in turn can be dumped to | ||
| files. Both PyQt4 and PyQt5 are supported. See ``anna.frontends.qt``. | ||
| Keywords: configuration framework | ||
| Platform: UNKNOWN | ||
| Classifier: Development Status :: 4 - Beta | ||
| Classifier: License :: OSI Approved :: BSD License | ||
| Classifier: Programming Language :: Python :: 2.7 | ||
| Classifier: Programming Language :: Python :: 3.5 | ||
| Classifier: Programming Language :: Python :: 3.6 | ||
| Classifier: Programming Language :: Python :: 3.7 | ||
| Classifier: Programming Language :: Python :: 3.8 | ||
| Classifier: Programming Language :: Python :: 3.9 | ||
| Classifier: Topic :: Software Development |
+295
| Anna - A Neat configuratioN Auxiliary | ||
| ===================================== | ||
| Anna helps you configure your application by building the bridge between the components of | ||
| your application and external configuration sources. It allows you to keep your code short and | ||
| flexible yet explicit when it comes to configuration - the necessary tinkering is performed by | ||
| the framework. | ||
| Anna contains lots of "in-place" documentation aka doc strings so make sure you check out those | ||
| too ("``help`` yourself")! | ||
| 80 seconds to Anna | ||
| ------------------ | ||
| Anna is all about *parameters* and *configuration sources*. You declare parameters as part of | ||
| your application (on a class for example) and specify their values in a configuration source. | ||
| All you're left to do with then is to point your application to the configuration source and | ||
| let the framework do its job. | ||
| An example is worth a thousand words | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| Say we want to build an application that deals with vehicles. I'm into cars so the first thing | ||
| I'll do is make sure we get one of those:: | ||
| >>> class Car: | ||
| ... def __init__(self, brand, model): | ||
| ... self._brand = brand | ||
| ... self._model = model | ||
| >>> | ||
| >>> your_car = Car('Your favorite brand', 'The hottest model') | ||
| Great! We let the user specify the car's ``brand`` and ``model`` and return him a brand new car! | ||
| Now we're using ``anna`` for declaring the parameters:: | ||
| >>> from anna import Configurable, parametrize, String, JSONAdaptor | ||
| >>> | ||
| >>> @parametrize( | ||
| ... String('Brand'), | ||
| ... String('Model') | ||
| ... ) | ||
| ... class Car(Configurable): | ||
| ... def __init__(self, config): | ||
| ... super(Car, self).__init__(config) | ||
| >>> | ||
| >>> your_car = Car(JSONAdaptor('the_file_where_you_specified_your_favorite_car.json')) | ||
| The corresponding json file would look like this:: | ||
| { | ||
| "Car/Parameters/Brand": "Your favorite brand", | ||
| "Car/Parameters/Model": "The hottest model", | ||
| } | ||
| It's a bit more to type but this comes at a few advantages: | ||
| * We can specify the type of the parameter and ``anna`` will handle the necessary conversions | ||
| for us; ``anna`` ships with plenty of parameter types so there's much more to it than just | ||
| strings! | ||
| * If we change your mind later on and want to add another parameter, say for example the color | ||
| of the car, it's as easy as declaring a new parameter ``String('Color')`` and setting it as | ||
| a class attribute; all the user needs to do is to specify the corresponding value in | ||
| the configuration source. Note that there's no need to change any interfaces/signatures or | ||
| other intermediate components which carry the user input to the receiving class; all it expects | ||
| is a configuration adaptor which points to the configuration source. | ||
| * The configuration source can host parameters for more than only one component, meaning again | ||
| that we don't need to modify intermediate parts when adding new components to our application; | ||
| all we need to do is provide the configuration adaptor. | ||
| Five minutes hands-on | ||
| --------------------- | ||
| The 80 seconds intro piqued your curiosity? Great! So let's move on! For the following | ||
| considerations we'll pick up the example from above and elaborate on it more thoroughly. | ||
| Let's start with a quick Q/A session | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| **So what happened when using the decorator ``parametrize``?** It received a number of parameters | ||
| as arguments which it set as attributes on the receiving class. Field names are deduced from | ||
| the parameters names applying CamelCase to _snake_case_with_leading_underscore conversion. | ||
| That is ``String('Brand')`` is set as ``Car._brand``. | ||
| **All right, but how did the instance receive its values then?** Note that ``Car`` inherits from | ||
| ``Configurable`` and ``Configurable.__init__`` is where the actual instance configuration happens. | ||
| We provided it a configuration adaptor which points to the configuration source (in this case | ||
| a local file) and the specific values were extracted from there. Values are set on the instance | ||
| using the parameter's field name, that is ``String('Brand')`` will make an instance receive | ||
| the corresponding value at ``your_car._brand`` (``Car._brand`` is still the parameter instance). | ||
| **Okay, but how did the framework know where to find the values in the configuration source?** | ||
| Well there's a bit more going on during the call to ``parametrize`` than is written above. | ||
| In addition to setting the parameters on the class it also deduces a configuration path for | ||
| each parameter which specifies where to find the corresponding value in the source. The path | ||
| consists of a base path and the parameter's name: "<base-path>/<name>" (slashes are used | ||
| to delimit path elements). ``parametrize`` tries to get this base path from the receiving class | ||
| looking up the attribute ``CONFIG_PATH``. If it has no such attribute or if it's ``None`` then | ||
| the base path defaults to "<class-name>/Parameters". However in our example - although we didn't | ||
| set the config path explicitly - it was already there because ``Configurable`` uses a custom | ||
| metaclass which adds the class attribute ``CONFIG_PATH`` if it's missing or ``None`` using | ||
| the same default as above. So if you want to specify a custom path within the source you can do so | ||
| by specifying the class attribute ``CONFIG_PATH``. | ||
| **_snake_case_with_leading_underscore, not too bad but can I choose custom field names for the parameters too?** | ||
| Yes, besides providing a number of parameters as arguments to ``parametrize`` we have the option | ||
| to supply it a number of keyword arguments as well which represent field_name / parameter pairs; | ||
| the key is the field name and the value is the parameter: ``brand_name=String('Brand')``. | ||
| **Now that we declared all those parameters how does the user know what to specify?** | ||
| ``anna`` provides a decorator ``document_parameters`` which will add all declared parameters to | ||
| the component's doc string under a new section. Another option for the user is to retrieve | ||
| the declared parameters via ``get_parameters`` (which is inherited from ``Configurable``) and | ||
| print their string representations which contain comprehensive information:: | ||
| >>> for parameter in Car.get_parameters(): | ||
| ... print(parameter) | ||
| Of course documenting the parameters manually is also an option. | ||
| Alright so let's get to the code | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| :: | ||
| >>> from anna import Configurable, parametrize, String, JSONAdaptor | ||
| >>> | ||
| >>> @parametrize( | ||
| ... String('Model'), | ||
| ... brand_name=String('Brand') | ||
| ... ) | ||
| ... class Car(Configurable): | ||
| ... CONFIG_PATH = 'Car' | ||
| ... def __init__(self, config): | ||
| ... super(Car, self).__init__(config) | ||
| Let's first see what information we can get about the parameters:: | ||
| >>> for parameter in Car.get_parameters(): | ||
| ... print(parameter) | ||
| ... | ||
| { | ||
| "optional": false, | ||
| "type": "StringParameter", | ||
| "name": "Model", | ||
| "path": "Car" | ||
| } | ||
| { | ||
| "optional": false, | ||
| "type": "StringParameter", | ||
| "name": "Brand", | ||
| "path": "Car" | ||
| } | ||
| Note that it prints ``"StringParameter"`` because that's the parameter's actual class, | ||
| ``String`` is just a shorthand. Let's see what we can get from the doc string:: | ||
| >>> print(Car.__doc__) | ||
| None | ||
| >>> from anna import document_parameters | ||
| >>> Car = document_parameters(Car) | ||
| >>> print(Car.__doc__) | ||
| Declared parameters | ||
| ------------------- | ||
| (configuration path: Car) | ||
| Brand : String | ||
| Model : String | ||
| Now that we know what we need to specify let's get us a car! The ``JSONAdaptor`` can also be | ||
| initialized with a ``dict`` as root element, so we're just creating our configuration on the fly:: | ||
| >>> back_to_the_future = JSONAdaptor(root={ | ||
| ... 'Car/Brand': 'DeLorean', | ||
| ... 'Car/Model': 'DMC-12', | ||
| ... }) | ||
| >>> doc_browns_car = Car(back_to_the_future) | ||
| >>> doc_browns_car.brand_name # Access via our custom field name. | ||
| 'DeLorean' | ||
| >>> doc_browns_car._model # Access via the automatically chosen field name. | ||
| 'DMC-12' | ||
| Creating another car is as easy as providing another configuration source:: | ||
| >>> mr_bonds_car = Car(JSONAdaptor(root={ | ||
| ... 'Car/Brand': 'Aston Martin', | ||
| ... 'Car/Model': 'DB5', | ||
| ... })) | ||
| Let's assume we want more information about the brand than just its name. We have nicely stored | ||
| all information in a database:: | ||
| >>> database = { | ||
| ... 'DeLorean': { | ||
| ... 'name': 'DeLorean', | ||
| ... 'founded in': 1975, | ||
| ... 'founded by': 'John DeLorean', | ||
| ... }, | ||
| ... 'Aston Martin': { | ||
| ... 'name': 'Aston Martin', | ||
| ... 'founded in': 1913, | ||
| ... 'founded by': 'Lionel Martin, Robert Bamford', | ||
| ... }} | ||
| We also have a database access function which we can use to load stuff from the database:: | ||
| >>> def load_from_database(key): | ||
| ... return database[key] | ||
| To load this database information instead of just the brand's name we only have to modify | ||
| the ``Car`` class to declare a new parameter: ``ActionParameter`` (or ``Action``). | ||
| An ``ActionParameter`` wraps another parameter and let's us specify an action which is applied to | ||
| the parameter's value when it's loaded. For our case that is:: | ||
| >>> from anna import ActionParameter | ||
| >>> Car.brand = ActionParameter(String('Brand'), load_from_database) | ||
| >>> doc_browns_car = Car(back_to_the_future) | ||
| >>> doc_browns_car.brand | ||
| {'founded by': 'John DeLorean', 'name': 'DeLorean', 'founded in': 1975} | ||
| >>> doc_browns_car.brand_name | ||
| 'DeLorean' | ||
| Note that we didn't need to provide a new configuration source as the new ``brand`` parameter is | ||
| based on the brand name which is already present. | ||
| Say we also want to obtain the year in which the model was first produced and we have a function | ||
| for exactly that purpose however it requires the brand name and model name as one string:: | ||
| >>> def first_produced_in(brand_and_model): | ||
| ... return {'DeLorean DMC-12': 1981, 'Aston Martin DB5': 1963}[brand_and_model] | ||
| That's not a problem because an ``ActionParameter`` type lets us combine multiple parameters:: | ||
| >>> Car.first_produced_in = ActionParameter( | ||
| ... String('Brand'), | ||
| ... lambda brand, model: first_produced_in('%s %s' % (brand, model)), | ||
| ... depends_on=('Model',)) | ||
| Other existing parameters, specified either by name of by reference via the keyword argument | ||
| ``depends_on``, are passed as additional arguments to the given action. | ||
| In the above example we declared parameters on a class using ``parametrize`` but you could as well | ||
| use parameter instances independently and load their values via ``load_from_configuration`` which | ||
| expects a configuration adaptor as well as a configuration path which localizes the parameter's | ||
| value. You also have the option to provide a specification directly via | ||
| ``load_from_representation``. This functions expects the specification as a unicode string and | ||
| additional (meta) data as a ``dict`` (a unit for ``PhysicalQuantities`` for example). | ||
| This introduction was meant to demonstrate the basic principles but there's much more to ``anna`` | ||
| (especially when it comes to parameter types)! So make sure to check out also the other parts | ||
| of the docs! | ||
| Parameter types | ||
| --------------- | ||
| A great variety of parameter types are here at your disposal: | ||
| * ``Bool`` | ||
| * ``Integer`` | ||
| * ``String`` | ||
| * ``Number`` | ||
| * ``Vector`` | ||
| * ``Duplet`` | ||
| * ``Triplet`` | ||
| * ``Tuple`` | ||
| * ``PhysicalQuantity`` | ||
| * ``Action`` | ||
| * ``Choice`` | ||
| * ``Group`` | ||
| * ``ComplementaryGroup`` | ||
| * ``SubstitutionGroup`` | ||
| Configuration adaptors | ||
| ---------------------- | ||
| Two adaptor types are provided: | ||
| * ``XMLAdaptor`` for connecting to xml files. | ||
| * ``JSONAdaptor`` for connecting to json files (following some additional conventions). | ||
| Generating configuration files | ||
| ------------------------------ | ||
| Configuration files can of course be created manually however ``anna`` also ships with a ``PyQt`` | ||
| frontend that can be integrated into custom applications. The frontend provides input forms for | ||
| all parameter types as well as for whole parametrized classes together with convenience methods for | ||
| turning the forms' values into configuration adaptor instances which in turn can be dumped to | ||
| files. Both PyQt4 and PyQt5 are supported. See ``anna.frontends.qt``. |
| [bdist_wheel] | ||
| universal = 1 | ||
| [egg_info] | ||
| tag_build = | ||
| tag_date = 0 | ||
+50
| from __future__ import unicode_literals | ||
| from setuptools import setup | ||
| def readme(): | ||
| with open('README.rst') as f: | ||
| return f.read() | ||
| def version(): | ||
| with open('anna/VERSION') as f: | ||
| return f.read() | ||
| setup( | ||
| name='anna', | ||
| version=version(), | ||
| description='A Neat configuratioN Auxiliary', | ||
| long_description=readme(), | ||
| classifiers=[ | ||
| 'Development Status :: 4 - Beta', | ||
| 'License :: OSI Approved :: BSD License', | ||
| 'Programming Language :: Python :: 2.7', | ||
| 'Programming Language :: Python :: 3.5', | ||
| 'Programming Language :: Python :: 3.6', | ||
| 'Programming Language :: Python :: 3.7', | ||
| 'Programming Language :: Python :: 3.8', | ||
| 'Programming Language :: Python :: 3.9', | ||
| 'Topic :: Software Development', | ||
| ], | ||
| keywords='configuration framework', | ||
| url='https://gitlab.com/Dominik1123/Anna', | ||
| author='Dominik Vilsmeier', | ||
| author_email='dominik.vilsmeier1123@gmail.com', | ||
| license='BSD-3-Clause', | ||
| packages=[ | ||
| 'anna', | ||
| 'anna.frontends', | ||
| 'anna.frontends.qt', | ||
| 'anna.unittests', | ||
| ], | ||
| install_requires=[ | ||
| 'docutils', | ||
| 'numpy', | ||
| 'scipy', | ||
| 'six', | ||
| ], | ||
| include_package_data=True, | ||
| zip_safe=False | ||
| ) |
+2
-1
@@ -8,3 +8,3 @@ # -*- coding: utf-8 -*- | ||
| from .input import Unit, Value | ||
| from .parameters import BoolParameter, IntegerParameter, StringParameter, NumberParameter,\ | ||
| from .parameters import BoolParameter, IntegerParameter, StringParameter, NumberParameter, FilepathParameter, \ | ||
| VectorParameter, DupletParameter, TripletParameter, PhysicalQuantityParameter, \ | ||
@@ -22,2 +22,3 @@ ActionParameter, ChoiceParameter, ParameterGroup, ComplementaryParameterGroup, \ | ||
| Integer = IntegerParameter | ||
| Filepath = FilepathParameter | ||
| String = StringParameter | ||
@@ -24,0 +25,0 @@ Number = NumberParameter |
@@ -520,2 +520,6 @@ # -*- coding: utf-8 -*- | ||
| class FilepathInput(StringInput): | ||
| PEER = parameters.FilepathParameter | ||
| class ChoiceInput(ParameterInput): | ||
@@ -522,0 +526,0 @@ PEER = parameters.ChoiceParameter |
+1
-1
@@ -1,1 +0,1 @@ | ||
| 0.3.5 | ||
| 0.4 |
-297
| Anna - A Neat configuratioN Auxiliary | ||
| ===================================== | ||
| Anna helps you configure your application by building the bridge between the components of | ||
| your application and external configuration sources. It allows you to keep your code short and | ||
| flexible yet explicit when it comes to configuration - the necessary tinkering is performed by | ||
| the framework. | ||
| Anna contains lots of "in-place" documentation aka doc strings so make sure you check out those | ||
| too ("``help`` yourself")! | ||
| 80 seconds to Anna | ||
| ------------------ | ||
| Anna is all about *parameters* and *configuration sources*. You declare parameters as part of | ||
| your application (on a class for example) and specify their values in a configuration source. | ||
| All you're left to do with then is to point your application to the configuration source and | ||
| let the framework do its job. | ||
| An example is worth a thousand words | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| Say we want to build an application that deals with vehicles. I'm into cars so the first thing | ||
| I'll do is make sure we get one of those:: | ||
| >>> class Car: | ||
| ... def __init__(self, brand, model): | ||
| ... self._brand = brand | ||
| ... self._model = model | ||
| >>> | ||
| >>> your_car = Car('Your favorite brand', 'The hottest model') | ||
| Great! We let the user specify the car's ``brand`` and ``model`` and return him a brand new car! | ||
| Now we're using ``anna`` for declaring the parameters:: | ||
| >>> from anna import Configurable, parametrize, String, JSONAdaptor | ||
| >>> | ||
| >>> @parametrize( | ||
| ... String('Brand'), | ||
| ... String('Model') | ||
| ... ) | ||
| ... class Car(Configurable): | ||
| ... def __init__(self, config): | ||
| ... super(Car, self).__init__(config) | ||
| >>> | ||
| >>> your_car = Car(JSONAdaptor('the_file_where_you_specified_your_favorite_car.json')) | ||
| The corresponding json file would look like this:: | ||
| { | ||
| "Car/Parameters/Brand": "Your favorite brand", | ||
| "Car/Parameters/Model": "The hottest model", | ||
| } | ||
| It's a bit more to type but this comes at a few advantages: | ||
| * We can specify the type of the parameter and ``anna`` will handle the necessary conversions | ||
| for us; ``anna`` ships with plenty of parameter types so there's much more to it than just | ||
| strings! | ||
| * If we change your mind later on and want to add another parameter, say for example the color | ||
| of the car, it's as easy as declaring a new parameter ``String('Color')`` and setting it as | ||
| a class attribute; all the user needs to do is to specify the corresponding value in | ||
| the configuration source. Note that there's no need to change any interfaces/signatures or | ||
| other intermediate components which carry the user input to the receiving class; all it expects | ||
| is a configuration adaptor which points to the configuration source. | ||
| * The configuration source can host parameters for more than only one component, meaning again | ||
| that we don't need to modify intermediate parts when adding new components to our application; | ||
| all we need to do is provide the configuration adaptor. | ||
| Five minutes hands-on | ||
| --------------------- | ||
| The 80 seconds intro piqued your curiosity? Great! So let's move on! For the following | ||
| considerations we'll pick up the example from above and elaborate on it more thoroughly. | ||
| Let's start with a quick Q/A session | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| **So what happened when using the decorator ``parametrize``?** It received a number of parameters | ||
| as arguments which it set as attributes on the receiving class. Field names are deduced from | ||
| the parameters names applying CamelCase to _snake_case_with_leading_underscore conversion. | ||
| That is ``String('Brand')`` is set as ``Car._brand``. | ||
| **All right, but how did the instance receive its values then?** Note that ``Car`` inherits from | ||
| ``Configurable`` and ``Configurable.__init__`` is where the actual instance configuration happens. | ||
| We provided it a configuration adaptor which points to the configuration source (in this case | ||
| a local file) and the specific values were extracted from there. Values are set on the instance | ||
| using the parameter's field name, that is ``String('Brand')`` will make an instance receive | ||
| the corresponding value at ``your_car._brand`` (``Car._brand`` is still the parameter instance). | ||
| **Okay, but how did the framework know where to find the values in the configuration source?** | ||
| Well there's a bit more going on during the call to ``parametrize`` than is written above. | ||
| In addition to setting the parameters on the class it also deduces a configuration path for | ||
| each parameter which specifies where to find the corresponding value in the source. The path | ||
| consists of a base path and the parameter's name: "<base-path>/<name>" (slashes are used | ||
| to delimit path elements). ``parametrize`` tries to get this base path from the receiving class | ||
| looking up the attribute ``CONFIG_PATH``. If it has no such attribute or if it's ``None`` then | ||
| the base path defaults to "<class-name>/Parameters". However in our example - although we didn't | ||
| set the config path explicitly - it was already there because ``Configurable`` uses a custom | ||
| metaclass which adds the class attribute ``CONFIG_PATH`` if it's missing or ``None`` using | ||
| the same default as above. So if you want to specify a custom path within the source you can do so | ||
| by specifying the class attribute ``CONFIG_PATH``. | ||
| **_snake_case_with_leading_underscore, not too bad but can I choose custom field names for the parameters too?** | ||
| Yes, besides providing a number of parameters as arguments to ``parametrize`` we have the option | ||
| to supply it a number of keyword arguments as well which represent field_name / parameter pairs; | ||
| the key is the field name and the value is the parameter: ``brand_name=String('Brand')``. | ||
| **Now that we declared all those parameters how does the user know what to specify?** | ||
| ``anna`` provides a decorator ``document_parameters`` which will add all declared parameters to | ||
| the component's doc string under a new section. Another option for the user is to retrieve | ||
| the declared parameters via ``get_parameters`` (which is inherited from ``Configurable``) and | ||
| print their string representations which contain comprehensive information:: | ||
| >>> for parameter in Car.get_parameters(): | ||
| ... print(parameter) | ||
| Of course documenting the parameters manually is also an option. | ||
| Alright so let's get to the code | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| :: | ||
| >>> from anna import Configurable, parametrize, String, JSONAdaptor | ||
| >>> | ||
| >>> @parametrize( | ||
| ... String('Model'), | ||
| ... brand_name=String('Brand') | ||
| ... ) | ||
| ... class Car(Configurable): | ||
| ... CONFIG_PATH = 'Car' | ||
| ... def __init__(self, config): | ||
| ... super(Car, self).__init__(config) | ||
| Let's first see what information we can get about the parameters:: | ||
| >>> for parameter in Car.get_parameters(): | ||
| ... print(parameter) | ||
| ... | ||
| { | ||
| "optional": false, | ||
| "type": "StringParameter", | ||
| "name": "Model", | ||
| "path": "Car" | ||
| } | ||
| { | ||
| "optional": false, | ||
| "type": "StringParameter", | ||
| "name": "Brand", | ||
| "path": "Car" | ||
| } | ||
| Note that it prints ``"StringParameter"`` because that's the parameter's actual class, | ||
| ``String`` is just a shorthand. Let's see what we can get from the doc string:: | ||
| >>> print(Car.__doc__) | ||
| None | ||
| >>> from anna import document_parameters | ||
| >>> Car = document_parameters(Car) | ||
| >>> print(Car.__doc__) | ||
| Declared parameters | ||
| ------------------- | ||
| (configuration path: Car) | ||
| Brand : String | ||
| Model : String | ||
| Now that we know what we need to specify let's get us a car! The ``JSONAdaptor`` can also be | ||
| initialized with a ``dict`` as root element, so we're just creating our configuration on the fly:: | ||
| >>> back_to_the_future = JSONAdaptor(root={ | ||
| ... 'Car/Brand': 'DeLorean', | ||
| ... 'Car/Model': 'DMC-12', | ||
| ... }) | ||
| >>> doc_browns_car = Car(back_to_the_future) | ||
| >>> doc_browns_car.brand_name # Access via our custom field name. | ||
| 'DeLorean' | ||
| >>> doc_browns_car._model # Access via the automatically chosen field name. | ||
| 'DMC-12' | ||
| Creating another car is as easy as providing another configuration source:: | ||
| >>> mr_bonds_car = Car(JSONAdaptor(root={ | ||
| ... 'Car/Brand': 'Aston Martin', | ||
| ... 'Car/Model': 'DB5', | ||
| ... })) | ||
| Let's assume we want more information about the brand than just its name. We have nicely stored | ||
| all information in a database:: | ||
| >>> database = { | ||
| ... 'DeLorean': { | ||
| ... 'name': 'DeLorean', | ||
| ... 'founded in': 1975, | ||
| ... 'founded by': 'John DeLorean', | ||
| ... }, | ||
| ... 'Aston Martin': { | ||
| ... 'name': 'Aston Martin', | ||
| ... 'founded in': 1913, | ||
| ... 'founded by': 'Lionel Martin, Robert Bamford', | ||
| ... }} | ||
| We also have a database access function which we can use to load stuff from the database:: | ||
| >>> def load_from_database(key): | ||
| ... return database[key] | ||
| To load this database information instead of just the brand's name we only have to modify | ||
| the ``Car`` class to declare a new parameter: ``ActionParameter`` (or ``Action``). | ||
| An ``ActionParameter`` wraps another parameter and let's us specify an action which is applied to | ||
| the parameter's value when it's loaded. For our case that is:: | ||
| >>> from anna import ActionParameter | ||
| >>> Car.brand = ActionParameter(String('Brand'), load_from_database) | ||
| >>> doc_browns_car = Car(back_to_the_future) | ||
| >>> doc_browns_car.brand | ||
| {'founded by': 'John DeLorean', 'name': 'DeLorean', 'founded in': 1975} | ||
| >>> doc_browns_car.brand_name | ||
| 'DeLorean' | ||
| Note that we didn't need to provide a new configuration source as the new ``brand`` parameter is | ||
| based on the brand name which is already present. | ||
| Say we also want to obtain the year in which the model was first produced and we have a function | ||
| for exactly that purpose however it requires the brand name and model name as one string:: | ||
| >>> def first_produced_in(brand_and_model): | ||
| ... return {'DeLorean DMC-12': 1981, 'Aston Martin DB5': 1963}[brand_and_model] | ||
| That's not a problem because an ``ActionParameter`` type lets us combine multiple parameters:: | ||
| >>> Car.first_produced_in = ActionParameter( | ||
| ... String('Brand'), | ||
| ... lambda brand, model: first_produced_in('%s %s' % (brand, model)), | ||
| ... depends_on=('Model',)) | ||
| Other existing parameters, specified either by name of by reference via the keyword argument | ||
| ``depends_on``, are passed as additional arguments to the given action. | ||
| In the above example we declared parameters on a class using ``parametrize`` but you could as well | ||
| use parameter instances independently and load their values via ``load_from_configuration`` which | ||
| expects a configuration adaptor as well as a configuration path which localizes the parameter's | ||
| value. You also have the option to provide a specification directly via | ||
| ``load_from_representation``. This functions expects the specification as a unicode string and | ||
| additional (meta) data as a ``dict`` (a unit for ``PhysicalQuantities`` for example). | ||
| This introduction was meant to demonstrate the basic principles but there's much more to ``anna`` | ||
| (especially when it comes to parameter types)! So make sure to check out also the other parts | ||
| of the docs! | ||
| Parameter types | ||
| --------------- | ||
| A great variety of parameter types are here at your disposal: | ||
| * ``Bool`` | ||
| * ``Integer`` | ||
| * ``String`` | ||
| * ``Number`` | ||
| * ``Vector`` | ||
| * ``Duplet`` | ||
| * ``Triplet`` | ||
| * ``Tuple`` | ||
| * ``PhysicalQuantity`` | ||
| * ``Action`` | ||
| * ``Choice`` | ||
| * ``Group`` | ||
| * ``ComplementaryGroup`` | ||
| * ``SubstitutionGroup`` | ||
| Configuration adaptors | ||
| ---------------------- | ||
| Two adaptor types are provided: | ||
| * ``XMLAdaptor`` for connecting to xml files. | ||
| * ``JSONAdaptor`` for connecting to json files (following some additional conventions). | ||
| Generating configuration files | ||
| ------------------------------ | ||
| Configuration files can of course be created manually however ``anna`` also ships with a ``PyQt`` | ||
| frontend that can be integrated into custom applications. The frontend provides input forms for | ||
| all parameter types as well as for whole parametrized classes together with convenience methods for | ||
| turning the forms' values into configuration adaptor instances which in turn can be dumped to | ||
| files. Both PyQt4 and PyQt5 are supported. See ``anna.frontends.qt``. | ||
-318
| Metadata-Version: 2.0 | ||
| Name: anna | ||
| Version: 0.3.5 | ||
| Summary: A Neat configuratioN Auxiliary | ||
| Home-page: https://gitlab.com/Dominik1123/Anna | ||
| Author: Dominik Vilsmeier | ||
| Author-email: dominik.vilsmeier1123@gmail.com | ||
| License: BSD-3-Clause | ||
| Keywords: configuration framework | ||
| Platform: UNKNOWN | ||
| Classifier: Development Status :: 4 - Beta | ||
| Classifier: License :: OSI Approved :: BSD License | ||
| Classifier: Programming Language :: Python :: 2.7 | ||
| Classifier: Programming Language :: Python :: 3.5 | ||
| Classifier: Programming Language :: Python :: 3.6 | ||
| Classifier: Topic :: Software Development | ||
| Requires-Dist: docutils | ||
| Requires-Dist: numpy | ||
| Requires-Dist: scipy | ||
| Requires-Dist: six | ||
| Anna - A Neat configuratioN Auxiliary | ||
| ===================================== | ||
| Anna helps you configure your application by building the bridge between the components of | ||
| your application and external configuration sources. It allows you to keep your code short and | ||
| flexible yet explicit when it comes to configuration - the necessary tinkering is performed by | ||
| the framework. | ||
| Anna contains lots of "in-place" documentation aka doc strings so make sure you check out those | ||
| too ("``help`` yourself")! | ||
| 80 seconds to Anna | ||
| ------------------ | ||
| Anna is all about *parameters* and *configuration sources*. You declare parameters as part of | ||
| your application (on a class for example) and specify their values in a configuration source. | ||
| All you're left to do with then is to point your application to the configuration source and | ||
| let the framework do its job. | ||
| An example is worth a thousand words | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| Say we want to build an application that deals with vehicles. I'm into cars so the first thing | ||
| I'll do is make sure we get one of those:: | ||
| >>> class Car: | ||
| ... def __init__(self, brand, model): | ||
| ... self._brand = brand | ||
| ... self._model = model | ||
| >>> | ||
| >>> your_car = Car('Your favorite brand', 'The hottest model') | ||
| Great! We let the user specify the car's ``brand`` and ``model`` and return him a brand new car! | ||
| Now we're using ``anna`` for declaring the parameters:: | ||
| >>> from anna import Configurable, parametrize, String, JSONAdaptor | ||
| >>> | ||
| >>> @parametrize( | ||
| ... String('Brand'), | ||
| ... String('Model') | ||
| ... ) | ||
| ... class Car(Configurable): | ||
| ... def __init__(self, config): | ||
| ... super(Car, self).__init__(config) | ||
| >>> | ||
| >>> your_car = Car(JSONAdaptor('the_file_where_you_specified_your_favorite_car.json')) | ||
| The corresponding json file would look like this:: | ||
| { | ||
| "Car/Parameters/Brand": "Your favorite brand", | ||
| "Car/Parameters/Model": "The hottest model", | ||
| } | ||
| It's a bit more to type but this comes at a few advantages: | ||
| * We can specify the type of the parameter and ``anna`` will handle the necessary conversions | ||
| for us; ``anna`` ships with plenty of parameter types so there's much more to it than just | ||
| strings! | ||
| * If we change your mind later on and want to add another parameter, say for example the color | ||
| of the car, it's as easy as declaring a new parameter ``String('Color')`` and setting it as | ||
| a class attribute; all the user needs to do is to specify the corresponding value in | ||
| the configuration source. Note that there's no need to change any interfaces/signatures or | ||
| other intermediate components which carry the user input to the receiving class; all it expects | ||
| is a configuration adaptor which points to the configuration source. | ||
| * The configuration source can host parameters for more than only one component, meaning again | ||
| that we don't need to modify intermediate parts when adding new components to our application; | ||
| all we need to do is provide the configuration adaptor. | ||
| Five minutes hands-on | ||
| --------------------- | ||
| The 80 seconds intro piqued your curiosity? Great! So let's move on! For the following | ||
| considerations we'll pick up the example from above and elaborate on it more thoroughly. | ||
| Let's start with a quick Q/A session | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| **So what happened when using the decorator ``parametrize``?** It received a number of parameters | ||
| as arguments which it set as attributes on the receiving class. Field names are deduced from | ||
| the parameters names applying CamelCase to _snake_case_with_leading_underscore conversion. | ||
| That is ``String('Brand')`` is set as ``Car._brand``. | ||
| **All right, but how did the instance receive its values then?** Note that ``Car`` inherits from | ||
| ``Configurable`` and ``Configurable.__init__`` is where the actual instance configuration happens. | ||
| We provided it a configuration adaptor which points to the configuration source (in this case | ||
| a local file) and the specific values were extracted from there. Values are set on the instance | ||
| using the parameter's field name, that is ``String('Brand')`` will make an instance receive | ||
| the corresponding value at ``your_car._brand`` (``Car._brand`` is still the parameter instance). | ||
| **Okay, but how did the framework know where to find the values in the configuration source?** | ||
| Well there's a bit more going on during the call to ``parametrize`` than is written above. | ||
| In addition to setting the parameters on the class it also deduces a configuration path for | ||
| each parameter which specifies where to find the corresponding value in the source. The path | ||
| consists of a base path and the parameter's name: "<base-path>/<name>" (slashes are used | ||
| to delimit path elements). ``parametrize`` tries to get this base path from the receiving class | ||
| looking up the attribute ``CONFIG_PATH``. If it has no such attribute or if it's ``None`` then | ||
| the base path defaults to "<class-name>/Parameters". However in our example - although we didn't | ||
| set the config path explicitly - it was already there because ``Configurable`` uses a custom | ||
| metaclass which adds the class attribute ``CONFIG_PATH`` if it's missing or ``None`` using | ||
| the same default as above. So if you want to specify a custom path within the source you can do so | ||
| by specifying the class attribute ``CONFIG_PATH``. | ||
| **_snake_case_with_leading_underscore, not too bad but can I choose custom field names for the parameters too?** | ||
| Yes, besides providing a number of parameters as arguments to ``parametrize`` we have the option | ||
| to supply it a number of keyword arguments as well which represent field_name / parameter pairs; | ||
| the key is the field name and the value is the parameter: ``brand_name=String('Brand')``. | ||
| **Now that we declared all those parameters how does the user know what to specify?** | ||
| ``anna`` provides a decorator ``document_parameters`` which will add all declared parameters to | ||
| the component's doc string under a new section. Another option for the user is to retrieve | ||
| the declared parameters via ``get_parameters`` (which is inherited from ``Configurable``) and | ||
| print their string representations which contain comprehensive information:: | ||
| >>> for parameter in Car.get_parameters(): | ||
| ... print(parameter) | ||
| Of course documenting the parameters manually is also an option. | ||
| Alright so let's get to the code | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
| :: | ||
| >>> from anna import Configurable, parametrize, String, JSONAdaptor | ||
| >>> | ||
| >>> @parametrize( | ||
| ... String('Model'), | ||
| ... brand_name=String('Brand') | ||
| ... ) | ||
| ... class Car(Configurable): | ||
| ... CONFIG_PATH = 'Car' | ||
| ... def __init__(self, config): | ||
| ... super(Car, self).__init__(config) | ||
| Let's first see what information we can get about the parameters:: | ||
| >>> for parameter in Car.get_parameters(): | ||
| ... print(parameter) | ||
| ... | ||
| { | ||
| "optional": false, | ||
| "type": "StringParameter", | ||
| "name": "Model", | ||
| "path": "Car" | ||
| } | ||
| { | ||
| "optional": false, | ||
| "type": "StringParameter", | ||
| "name": "Brand", | ||
| "path": "Car" | ||
| } | ||
| Note that it prints ``"StringParameter"`` because that's the parameter's actual class, | ||
| ``String`` is just a shorthand. Let's see what we can get from the doc string:: | ||
| >>> print(Car.__doc__) | ||
| None | ||
| >>> from anna import document_parameters | ||
| >>> Car = document_parameters(Car) | ||
| >>> print(Car.__doc__) | ||
| Declared parameters | ||
| ------------------- | ||
| (configuration path: Car) | ||
| Brand : String | ||
| Model : String | ||
| Now that we know what we need to specify let's get us a car! The ``JSONAdaptor`` can also be | ||
| initialized with a ``dict`` as root element, so we're just creating our configuration on the fly:: | ||
| >>> back_to_the_future = JSONAdaptor(root={ | ||
| ... 'Car/Brand': 'DeLorean', | ||
| ... 'Car/Model': 'DMC-12', | ||
| ... }) | ||
| >>> doc_browns_car = Car(back_to_the_future) | ||
| >>> doc_browns_car.brand_name # Access via our custom field name. | ||
| 'DeLorean' | ||
| >>> doc_browns_car._model # Access via the automatically chosen field name. | ||
| 'DMC-12' | ||
| Creating another car is as easy as providing another configuration source:: | ||
| >>> mr_bonds_car = Car(JSONAdaptor(root={ | ||
| ... 'Car/Brand': 'Aston Martin', | ||
| ... 'Car/Model': 'DB5', | ||
| ... })) | ||
| Let's assume we want more information about the brand than just its name. We have nicely stored | ||
| all information in a database:: | ||
| >>> database = { | ||
| ... 'DeLorean': { | ||
| ... 'name': 'DeLorean', | ||
| ... 'founded in': 1975, | ||
| ... 'founded by': 'John DeLorean', | ||
| ... }, | ||
| ... 'Aston Martin': { | ||
| ... 'name': 'Aston Martin', | ||
| ... 'founded in': 1913, | ||
| ... 'founded by': 'Lionel Martin, Robert Bamford', | ||
| ... }} | ||
| We also have a database access function which we can use to load stuff from the database:: | ||
| >>> def load_from_database(key): | ||
| ... return database[key] | ||
| To load this database information instead of just the brand's name we only have to modify | ||
| the ``Car`` class to declare a new parameter: ``ActionParameter`` (or ``Action``). | ||
| An ``ActionParameter`` wraps another parameter and let's us specify an action which is applied to | ||
| the parameter's value when it's loaded. For our case that is:: | ||
| >>> from anna import ActionParameter | ||
| >>> Car.brand = ActionParameter(String('Brand'), load_from_database) | ||
| >>> doc_browns_car = Car(back_to_the_future) | ||
| >>> doc_browns_car.brand | ||
| {'founded by': 'John DeLorean', 'name': 'DeLorean', 'founded in': 1975} | ||
| >>> doc_browns_car.brand_name | ||
| 'DeLorean' | ||
| Note that we didn't need to provide a new configuration source as the new ``brand`` parameter is | ||
| based on the brand name which is already present. | ||
| Say we also want to obtain the year in which the model was first produced and we have a function | ||
| for exactly that purpose however it requires the brand name and model name as one string:: | ||
| >>> def first_produced_in(brand_and_model): | ||
| ... return {'DeLorean DMC-12': 1981, 'Aston Martin DB5': 1963}[brand_and_model] | ||
| That's not a problem because an ``ActionParameter`` type lets us combine multiple parameters:: | ||
| >>> Car.first_produced_in = ActionParameter( | ||
| ... String('Brand'), | ||
| ... lambda brand, model: first_produced_in('%s %s' % (brand, model)), | ||
| ... depends_on=('Model',)) | ||
| Other existing parameters, specified either by name of by reference via the keyword argument | ||
| ``depends_on``, are passed as additional arguments to the given action. | ||
| In the above example we declared parameters on a class using ``parametrize`` but you could as well | ||
| use parameter instances independently and load their values via ``load_from_configuration`` which | ||
| expects a configuration adaptor as well as a configuration path which localizes the parameter's | ||
| value. You also have the option to provide a specification directly via | ||
| ``load_from_representation``. This functions expects the specification as a unicode string and | ||
| additional (meta) data as a ``dict`` (a unit for ``PhysicalQuantities`` for example). | ||
| This introduction was meant to demonstrate the basic principles but there's much more to ``anna`` | ||
| (especially when it comes to parameter types)! So make sure to check out also the other parts | ||
| of the docs! | ||
| Parameter types | ||
| --------------- | ||
| A great variety of parameter types are here at your disposal: | ||
| * ``Bool`` | ||
| * ``Integer`` | ||
| * ``String`` | ||
| * ``Number`` | ||
| * ``Vector`` | ||
| * ``Duplet`` | ||
| * ``Triplet`` | ||
| * ``Tuple`` | ||
| * ``PhysicalQuantity`` | ||
| * ``Action`` | ||
| * ``Choice`` | ||
| * ``Group`` | ||
| * ``ComplementaryGroup`` | ||
| * ``SubstitutionGroup`` | ||
| Configuration adaptors | ||
| ---------------------- | ||
| Two adaptor types are provided: | ||
| * ``XMLAdaptor`` for connecting to xml files. | ||
| * ``JSONAdaptor`` for connecting to json files (following some additional conventions). | ||
| Generating configuration files | ||
| ------------------------------ | ||
| Configuration files can of course be created manually however ``anna`` also ships with a ``PyQt`` | ||
| frontend that can be integrated into custom applications. The frontend provides input forms for | ||
| all parameter types as well as for whole parametrized classes together with convenience methods for | ||
| turning the forms' values into configuration adaptor instances which in turn can be dumped to | ||
| files. Both PyQt4 and PyQt5 are supported. See ``anna.frontends.qt``. | ||
| {"classifiers": ["Development Status :: 4 - Beta", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Software Development"], "extensions": {"python.details": {"contacts": [{"email": "dominik.vilsmeier1123@gmail.com", "name": "Dominik Vilsmeier", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://gitlab.com/Dominik1123/Anna"}}}, "extras": [], "generator": "bdist_wheel (0.29.0)", "keywords": ["configuration", "framework"], "license": "BSD-3-Clause", "metadata_version": "2.0", "name": "anna", "run_requires": [{"requires": ["docutils", "numpy", "scipy", "six"]}], "summary": "A Neat configuratioN Auxiliary", "version": "0.3.5"} |
-32
| anna/LICENSE,sha256=67qDAERIVD2KKukzHRYN-2EPEvBD4mKJUlcF1Hlvts4,1473 | ||
| anna/VERSION,sha256=chs29ukegsplmSIlFfRBz4srybz6fhCYDk5F04KGouQ,5 | ||
| anna/__init__.py,sha256=EJt0kRbAZZgwNbMV_qz_XdGUvbgcn2eVe6ATOKO_USA,1115 | ||
| anna/adaptors.py,sha256=wtz95KeQajZJj_8BkJdOaurcFSQHi9sqYNaARSz7oVo,71683 | ||
| anna/configuration.py,sha256=Gm5GiesQqYndnZAmDJfnToL4JabM32-iyQ1MFm2VAKU,21371 | ||
| anna/datatypes.py,sha256=Pm1OMbPE7q-e_18gb60Jg-tZtQ8p6DdGblHn43hNBpQ,843 | ||
| anna/dependencies.py,sha256=AoPDfArK5p-JaW7ZvFJVdJ-o2HTa1er-gR6CbmRHXTI,560 | ||
| anna/exceptions.py,sha256=b75LNzTWmyUkW5a-N4oRSlofoDZpukOshiXtIFuqf-Y,1842 | ||
| anna/input.py,sha256=hB5DmrHT5RPCd_nb-j1_-vob9SBC9o492pV9pzjwRoo,16481 | ||
| anna/parameters.py,sha256=9Ftwr5e0S8OxRfy-OhQcP3-3_0-7kMQlM2IzVceNgm4,91852 | ||
| anna/utils.py,sha256=Ac3zADfTlVIu5DnqLaNSPzvKOBtHr0H7ngW4MJnXs_E,6026 | ||
| anna/frontends/__init__.py,sha256=_I2jRtsANdiSriucd1gJP27kuWpGPpRDjLPXcREc7FQ,42 | ||
| anna/frontends/qt/__init__.py,sha256=ygjQDGmSEP9hMjedgtql6IVFr0JqCrSprQEEdQqHflI,393 | ||
| anna/frontends/qt/dialogs.py,sha256=PngH8bKY66zFUxrLylTr6PKNaIBVMOcDE2encu_s_X4,952 | ||
| anna/frontends/qt/forms.py,sha256=16ws5YCoIjO79blvSLU_Jt1VF4OtoAkAsaSeFd3Szxw,4327 | ||
| anna/frontends/qt/parameters.py,sha256=y_7B5neR6lXGImcg80LcO7fe4TY2uTTucZKD3XtdMOI,43881 | ||
| anna/frontends/qt/pyqt45.py,sha256=xKznlgVGOjKAFo998XmJvj6fAMSZJ4ZokwpcCZeT8f4,1890 | ||
| anna/frontends/qt/utils.py,sha256=jZKAvHJbHm-18UopK8QDYQvzXByKMYqb3MskAnNtqzw,1633 | ||
| anna/frontends/qt/views.py,sha256=vyfqaPs05eaYLgaMQ-cHBbcHGIp-ncN7pLXk_pjX1NM,4941 | ||
| anna/frontends/qt/widgets.py,sha256=s5hjyItFqp6cGo7vFYb7dLXn-2POiY8yYxLVQUKg7QU,4046 | ||
| anna/frontends/qt/icons/info.png,sha256=_AMMStLYIcZEruN1AYMceuAeym2gVcbTiFwmibSTbEg,3420 | ||
| anna/frontends/qt/icons/parameter_info.png,sha256=VtOibXjNCTqd7N6xdBl2r943IcZjR6ImcRUvL69518w,1103 | ||
| anna/unittests/__init__.py,sha256=iwhKnzeBJLKxpRVjvzwiRE63_zNpIBfaKLITauVph-0,24 | ||
| anna/unittests/adaptors.py,sha256=6VvyVUmjf79sdAHLIyLyJf0tAFdbYoXQlyEmGMYKiBg,27121 | ||
| anna/unittests/configuration.py,sha256=kniwziwSBZ4ZUmb3CFFLNzEtsnvL009raUjAwyqeDrc,2417 | ||
| anna/unittests/parameters.py,sha256=D7OCVLBpDHkBYpwYvmeJHLM3kJ0bryKKpjRV69mFrAo,25337 | ||
| anna-0.3.5.dist-info/DESCRIPTION.rst,sha256=DJuXbPBcZ06UVvEysZi517RE8cfRDh4qvmhLvZ24KCg,12336 | ||
| anna-0.3.5.dist-info/METADATA,sha256=3G8Vq5ZdwC31ajXLhZ1kliLhLmeIK9i2yue9fC9GvtA,12989 | ||
| anna-0.3.5.dist-info/RECORD,, | ||
| anna-0.3.5.dist-info/WHEEL,sha256=o2k-Qa-RMNIJmUdIc7KU6VWR_ErNRbWNlxDIpl7lm34,110 | ||
| anna-0.3.5.dist-info/metadata.json,sha256=JPj-omesU57_z9B2s_NesoC87b1QpFPW94LTZbcID8w,812 | ||
| anna-0.3.5.dist-info/top_level.txt,sha256=AH08HTKNg-vmDkgfxPeeseJg7dlgoetjQe8zCgyBJqY,5 |
| anna |
-6
| Wheel-Version: 1.0 | ||
| Generator: bdist_wheel (0.29.0) | ||
| Root-Is-Purelib: true | ||
| Tag: py2-none-any | ||
| Tag: py3-none-any | ||
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
414304
Infinity%40
Infinity%8884
Infinity%