Compare commits

...

42 commits

Author SHA1 Message Date
1a31ada869 bump: version 5.1.0rc0 → 5.1.0 2024-11-06 17:53:12 +01:00
4aecf2c4bd bump: version 5.0.0 → 5.1.0rc0 2024-11-06 08:32:15 +01:00
a3228fbf30 feat: add min_len, max_len, forbidden_char for password option 2024-11-06 08:31:37 +01:00
c3bb590415 bump: version 5.0.0rc0 → 5.0.0 2024-11-01 09:34:48 +01:00
16a7857d0c fix: add changelog_merge_prerelease to commitizen 2024-11-01 09:34:44 +01:00
952069a865 bump: version 4.1.0 → 5.0.0rc0 2024-11-01 08:54:35 +01:00
5e66d2074d fix: translation + black 2024-11-01 08:47:48 +01:00
b5d477a439 feat: return propertyerror in function 2024-10-25 22:15:45 +02:00
54ac0c980a fix: regexp tests 2024-10-25 22:15:24 +02:00
be8b1e7e4f fix: update documentation 2024-10-22 12:32:09 +02:00
7ae1b48f4a fix: better permissive support 2024-10-22 11:05:03 +02:00
7761758096 feat: mandatory for a variable and replace suffix to identifier 2024-10-22 10:25:15 +02:00
bbec439a5e default and default_multi is in value 2024-08-29 08:17:06 +02:00
9c36fb8fb2 symlink could be used in a leadership 2024-08-07 08:55:43 +02:00
b5346f9c57 do not translate type + remove doc (it's now description(uncalculated=True) + add option.followers() + export append/remove property informations 2024-08-02 10:54:47 +02:00
f4f5fb79e4 2023 => 2024 2024-07-06 14:33:25 +02:00
c2e3bf86f1 add uncalculated and only_self option in api 2024-07-06 14:31:15 +02:00
f2893aaacd support informations attribute for all options 2024-07-05 19:35:39 +02:00
4e1053bba9 calculation with self information and symlink 2024-07-04 19:55:49 +02:00
897d4dd216 aa 2024-06-27 08:39:28 +02:00
8e743131cd feat: reorganise code 2024-04-24 15:39:17 +02:00
ccf8ebc91e fix: 4.1.0 2024-02-21 21:05:14 +01:00
ff4f5abae4 bump: version 4.0.1 → 4.0.2 2024-02-20 21:48:11 +01:00
7f2d6b03e0 fix: sub dynamic in sub dynamic family 2024-02-20 21:44:06 +01:00
93fa26f8df feat: documentation 2023-12-17 21:22:52 +01:00
428e243630 4.0.1 2023-12-14 21:39:43 +01:00
1a5e4e37d1 api: get leader from leadership 2023-12-11 19:38:59 +01:00
de9485b74e better display function support 2023-12-11 19:38:35 +01:00
90564d4983 dynoptiondescription inside dynoptiondescription 2023-12-04 17:46:46 +01:00
6b473e63f2 feat: dynamic family can have sub family 2023-11-19 13:41:20 +01:00
059c5f7407 undefined is no more a valid value and Calculation could be a valid value 2023-11-18 21:12:13 +01:00
73c8db5839 do not add suffix for suboption in dynamic optiondescription 2023-11-17 22:39:33 +01:00
4b052c3943 better multi support 2023-11-15 21:44:15 +01:00
0b2f13404c allow more properties for leader 2023-11-15 21:43:58 +01:00
c74c346f09 better debug 2023-11-15 21:42:45 +01:00
e09ca78487 french translation 2023-11-15 21:41:37 +01:00
0b1d1ef3f1 add value.get() to optiondescription (instead of value.dict()) 2023-11-15 21:41:04 +01:00
e45a1910d9 expose function_waiting_for_dict 2023-11-15 21:40:16 +01:00
a3d04c7451 can personalise ALLOWED_LEADER_PROPERTIES variables 2023-11-04 08:29:32 +01:00
32b24c2978 better error message 2023-11-04 08:28:54 +01:00
a3261abc94 if an option has configerror for mandatory property but is disabled too, do not raise 2023-11-04 08:27:22 +01:00
e7b174f28f verify if value is a list for multi variables 2023-08-01 14:49:52 +02:00
151 changed files with 21652 additions and 16302 deletions

6
.cz.toml Normal file
View file

@ -0,0 +1,6 @@
[tool.commitizen]
name = "cz_conventional_commits"
tag_format = "$version"
version_scheme = "semver"
version = "4.1.0"
update_changelog_on_bump = true

1
.gitignore vendored
View file

@ -1,6 +1,5 @@
*~
*#
*.pyc
*.mo
*.swp
build/

32
.readthedocs.yaml Normal file
View file

@ -0,0 +1,32 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the OS, Python version and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.12"
# You can also specify other tool versions:
# nodejs: "19"
# rust: "1.64"
# golang: "1.19"
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: docs/conf.py
# Optionally build your docs in additional formats such as PDF and ePub
# formats:
# - pdf
# - epub
# Optional but recommended, declare the Python requirements required
# to build your documentation
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: docs/requirements.txt

40
CHANGELOG.md Normal file
View file

@ -0,0 +1,40 @@
## 5.1.0rc0 (2024-11-06)
### Feat
- add min_len, max_len, forbidden_char for password option
## 5.0.0 (2024-11-01)
### Fix
- add changelog_merge_prerelease to commitizen
## 5.0.0rc0 (2024-11-01)
### Feat
- return propertyerror in function
- mandatory for a variable and replace suffix to identifier
- reorganise code
### Fix
- translation + black
- regexp tests
- update documentation
- better permissive support
- 4.1.0
## 4.0.2 (2024-02-20)
### Fix
- sub dynamic in sub dynamic family
## 4.0.1 (2023-12-17)
### Feat
- documentation
- dynamic family can have sub family

View file

@ -1,3 +1,9 @@
Mon Dec 14 21:39:30 2023 +0200 Emmanuel Garette <egarette@silique.fr>
* add mission dependency for setuptools
Mon Dec 12 20:30:30 2023 +0200 Emmanuel Garette <egarette@silique.fr>
* version 4.0
Mon Apr 1 11:47:30 2019 +0200 Emmanuel Garette <egarette@cadoles.com>
* version 3.0 rc16
* tiramisu is now async

View file

@ -1,8 +0,0 @@
# Include the README
include *.rst
# Include the license file
include LICENSE.txt
# Include the data files
recursive-include tiramisu *.py *.mo

View file

@ -1,13 +1,25 @@
![Logo Tiramisu](logo.png "logo Tiramisu")
An options controller tool
-------------------------------------
Due to more and more available options required to set up an operating system,
compiler options or whatever, it became quite annoying to hand the necessary
options to where they are actually used and even more annoying to add new
options. To circumvent these problems the configuration control was
introduced...
Tiramisu is an options handler and an options controller, wich aims at
producing flexible and fast options access.
[Documentations](doc/README.md)
# LICENSES
See COPYING for the licences of the code and the documentation.
See [COPYING](COPYING) for the licences of the code and the documentation.
See AUTHORS for the details about the tiramisu's team.
See [AUTHORS](AUTHORS) for the details about the tiramisu's team.

View file

@ -1,350 +0,0 @@
# Browse the Config
## Getting the options
Create a simple Config:
```python
from asyncio import run
from tiramisu import Config
from tiramisu import StrOption, OptionDescription
# let's declare some options
var1 = StrOption('var1', 'first option')
# an option with a default value
var2 = StrOption('var2', 'second option', 'value')
# let's create a group of options
od1 = OptionDescription('od1', 'first OD', [var1, var2])
# let's create another group of options
rootod = OptionDescription('rootod', '', [od1])
async def main():
# let's create the config
cfg = await Config(rootod)
# the api is read only
await cfg.property.read_only()
# the read_write api is available
await cfg.property.read_write()
return cfg
cfg = run(main())
```
We retrieve by path an option named "var1" and then we retrieve its name and its docstring:
```python
async def main():
print(await cfg.option('od1.var1').option.name())
print(await cfg.option('od1.var1').option.doc())
run(main())
```
returns:
```
var1
first option
```
## Accessing the values of the options
Let's browse the configuration structure and option values.
You have getters as a "get" method on option objects:
1. getting all the options
```python
async def main():
print(await cfg.value.dict())
run(main())
```
returns:
```
{'var1': None, 'var2': 'value'}
```
2. getting the "od1" option description
```python
async def main():
print(await cfg.option('od1').value.dict())
run(main())
```
returns:
```
{'od1.var1': None, 'od1.var2': 'value'}
```
3. getting the var1 option's value
```python
async def main():
print(await cfg.option('od1.var1').value.get())
run(main())
```
returns:
```
None
```
4. getting the var2 option's default value
```python
async def main():
print(await cfg.option('od1.var2').value.get())
run(main())
```
returns:
```
value
```
5. trying to get a non existent option's value
```python
async def main():
try:
await cfg.option('od1.idontexist').value.get()
except AttributeError as err:
print(str(err))
run(main())
```
returns:
```
unknown option "idontexist" in optiondescription "first OD"
```
## Setting the value of an option
An important part of the setting's configuration consists of setting the
value's option.
You have setters as a "set" method on option objects.
And if you wanna come back to a default value, use the "reset()" method.
1. changing the "od1.var1" value
```python
async def main():
await cfg.option('od1.var1').value.set('éééé')
print(await cfg.option('od1.var1').value.get())
run(main())
```
returns:
```
éééé
```
2. carefull to the type of the value to be set
```python
async def main():
try:
await cfg.option('od1.var1').value.set(23454)
except ValueError as err:
print(str(err))
run(main())
```
returns:
```
"23454" is an invalid string for "first option"
```
3. let's come back to the default value
```python
async def main():
await cfg.option('od1.var2').value.reset()
print(await cfg.option('od1.var2').value.get())
run(main())
```
returns:
```
value
```
> **_Important_** If the config is "read only", setting an option's value isn't allowed, see [property](property.md).
Let's make the protocol of accessing a Config's option explicit
(because explicit is better than implicit):
1. If the option has not been declared, an "Error" is raised,
2. If an option is declared, but neither a value nor a default value has
been set, the returned value is "None",
3. If an option is declared and a default value has been set, but no value
has been set, the returned value is the default value of the option,
4. If an option is declared, and a value has been set, the returned value is
the value of the option.
But there are special exceptions. We will see later on that an option can be a
mandatory option. A mandatory option is an option that must have a value
defined.
Searching for an option
~~~~~~~~~~~~~~~~~~~~~~~~~~
In an application, knowing the path of an option is not always feasible.
That's why a tree of options can easily be searched with the "find()" method.
```python
from asyncio import run
from tiramisu import Config
from tiramisu import OptionDescription, StrOption
from tiramisu.setting import undefined
var1 = StrOption('var1', '')
var2 = StrOption('var2', '')
var3 = StrOption('var3', '')
od1 = OptionDescription('od1', '', [var1, var2, var3])
var4 = StrOption('var4', '')
var5 = StrOption('var5', '')
var6 = StrOption('var6', '')
var7 = StrOption('var1', '', 'value')
od2 = OptionDescription('od2', '', [var4, var5, var6, var7])
rootod = OptionDescription('rootod', '', [od1, od2])
```
Let's find an option by it's name
And let's find first an option by it's name
The search can be performed in a subtree
```python
async def main():
cfg = await Config(rootod)
print(await cfg.option.find(name='var1'))
run(main())
```
returns:
```
[<tiramisu.api.TiramisuOption object at 0x7f490a530f98>, <tiramisu.api.TiramisuOption object at 0x7f490a530748>]
```
If the option name is unique, the search can be stopped once one matched option has been found:
```python
async def main():
cfg = await Config(rootod)
print(await cfg.option.find(name='var1', first=True))
run(main())
```
returns:
```
<tiramisu.api.TiramisuOption object at 0x7ff27fc93c70>
```
Search object behaves like a cfg object, for example:
```python
async def main():
cfg = await Config(rootod)
option = await cfg.option.find(name='var1', first=True)
print(await option.option.name())
print(await option.option.doc())
run(main())
```
returns:
```
var1
var1
```
Search can be made with various criteria:
```python
async def main():
cfg = await Config(rootod)
await cfg.option.find(name='var3', value=undefined)
await cfg.option.find(name='var3', type=StrOption)
run(main())
```
The find method can be used in subconfigs:
```python
async def main():
cfg = await Config(rootod)
print(await cfg.option('od2').find('var1'))
run(main())
```
## The "dict" flattening utility
In a config or a subconfig, you can print a dict-like representation
In a "fullpath" or a "flatten" way
- get the "od1" option description:
```python
async def main():
cfg = await Config(rootod)
print(await cfg.option('od1').value.dict(fullpath=True))
run(main())
```
returns:
```
{'od1.var1': None, 'od1.var2': None, 'od1.var3': None}
```
```python
async def main():
cfg = await Config(rootod)
print(await cfg.option('od1').value.dict(fullpath=False))
run(main())
>>> print(cfg.option('od1').value.dict(fullpath=True))
```
returns:
```
{'var1': None, 'var2': None, 'var3': None}
```
> **_NOTE_** be carefull with this "flatten" parameter, because we can just loose some options if there are same name (some option can be overriden).

View file

@ -1,107 +0,0 @@
# The Config
Tiramisu is made of almost three main classes/concepts :
- the "Option" stands for the option types
- the "OptionDescription" is the schema, the option's structure
- the "Config" which is the whole configuration entry point
![The Config](config.png "The Config")
## The handling of options
The handling of options is split into two parts: the description of
which options are available, what their possible values and defaults are
and how they are organized into a tree. A specific choice of options is
bundled into a configuration object which has a reference to its option
description (and therefore makes sure that the configuration values
adhere to the option description).
- [Instanciate an option](option.md)
- [Default Options](options.md)
- [The symbolic link option: SymLinkOption](symlinkoption.md)
- [Create it's own option](own_option.md)
## Option description are nested Options
The Option (in this case the "BoolOption"),
are organized into a tree into nested "OptionDescription" objects.
Every option has a name, as does every option group.
- [Generic container: OptionDescription](optiondescription.md)
- [Dynamic option description: DynOptionDescription](dynoptiondescription.md)
- [Leadership OptionDescription: Leadership](leadership.md)
## Config
Getting started with the tiramisu library (it loads and prints properly).
Let's perform a *Getting started* code review :
```python
from asyncio import run
from tiramisu import Config
from tiramisu import OptionDescription, BoolOption
# let's create a group of options named "optgroup"
descr = OptionDescription("optgroup", "", [
# ... with only one option inside
BoolOption("bool", "", default=False)
])
async def main():
# Then, we make a Config with the OptionDescription` we
cfg = await Config(descr)
# the global help about the config
cfg.help()
run(main())
```
returns:
```
Root config object that enables us to handle the configuration options
Settings:
forcepermissive Access to option without verifying permissive properties
unrestraint Access to option without property restriction
Commands:
cache Manage config cache
config Actions to Config
information Manage config informations
option Select an option
owner Global owner
permissive Manage config permissives
property Manage config properties
session Manage Config session
value Manage config value
```
Then let's print our "Option details.
```python
cfg.option.help()
```
returns:
```
Select an option
Call: Select an option by path
Commands:
dict Convert config and option to tiramisu format
find Find an or a list of options
list List options (by default list only option)
updates Updates value with tiramisu format
```
## Go futher with "Option" and "Config"
- [property](property.md)
- [validator](validator.md)

View file

@ -1,57 +0,0 @@
# Dynamic option description: DynOptionDescription
## Dynamic option description's description
Dynamic option description is an OptionDescription which multiplies according to the return of a function.
First of all, an option description is a name, a description, children and suffixes.
### name
The "name" is important to retrieve this dynamic option description.
### description
The "description" allows the user to understand where this dynamic option description will contains.
### children
List of children option.
> **_NOTE:_** the option has to be multi option or leadership but not option description.
### suffixes
Suffixes is a [calculation](calculation.md) that return the list of suffixes used to create dynamic option description.
Let's try:
```python
from tiramisu import StrOption, DynOptionDescription, Calculation
def return_suffixes():
return ['1', '2']
child1 = StrOption('first', 'First basic option ')
child2 = StrOption('second', 'Second basic option ')
DynOptionDescription('basic ',
'Basic options ',
[child1, child2],
Calculation(return_suffixes))
```
This example will construct:
- Basic options 1:
- First basic option 1
- Second basic option 1
- Basic options 2:
- First basic option 2
- Second basic option 2
## Dynamic option description's properties
See [property](property.md).

View file

@ -1,45 +0,0 @@
# Leadership OptionDescription: Leadership
A leadership is a special "OptionDescription" that wait a leader and one or multiple followers.
Leader and follower are multi option. The difference is that the length is defined by the length of the option leader.
If the length of leader is 3, all followers have also length 3.
An other different is that the follower is isolate. That means that you can only change on value on a specified index in the list of it's values.
If a value is mark as modified in a specified index, that not affect the other values in other index.
## The leadership's description
A leadership is an "OptionDescription"
First of all, an option leadership is a name, a description and children.
### name
The "name" is important to retrieve this dynamic option description.
### description
The "description" allows the user to understand where this dynamic option description will contains.
### children
List of children option.
> **_NOTE:_** the option has to be multi or submulti option and not other option description.
Let's try:
```python
from tiramisu import StrOption, Leadership
users = StrOption('users', 'User', multi=True)
passwords = StrOption('passwords', 'Password', multi=True)
Leadership('users',
'User allow to connect',
[users, passwords])
```
## The leadership's properties
See [property](property.md).

View file

@ -1,134 +0,0 @@
# Instanciate an option
## Option's description
First of all, an option is a name and a description.
### name
The "name" is important to retrieve this option.
### description
The "description" allows the user to understand where this option will be used for.
Let's try:
```python
from tiramisu import StrOption
StrOption('welcome',
'Welcome message to the user login')
```
## Option's default value
For each option, we can defined a default value. This value will be the value of this option until user customize it.
This default value is store directly in the option. So we can, at any moment we can go back to the default value.
```python
StrOption('welcome',
'Welcome message to the user login',
'Hey guys, welcome here!')
```
The default value can be a calculation.
```python
from tiramisu import Calculation
def get_value():
return 'Hey guys, welcome here'
StrOption('welcome',
'Welcome message to the user login',
Calculation(get_value))
```
## Option with multiple value
### multi
There are cases where it can be interesting to have a list of values rather than just one.
The "multi" attribut is here for that.
In this case, the default value has to be a list:
```python
StrOption('shopping_list',
'The shopping list',
['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'],
multi=True)
```
The option could be a list of list, which is could submulti:
```python
from tiramisu import submulti
StrOption('shopping_list',
'The shopping list',
[['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'], ['milk', 'eggs']],
multi=submulti)
```
The default value can be a calculation. For a multi, the function have to return a list or have to be in a list:
```python
def get_values():
return ['1 kilogram of carrots', 'leeks', '1 kilogram of potatos']
StrOption('shopping_list',
'The shopping list',
Calculation(get_values),
multi=True)
```
```python
def get_a_value():
return 'leeks'
StrOption('shopping_list',
'The shopping list',
['1 kilogram of carrots', Calculation(get_a_value), '1 kilogram of potatos'],
multi=True)
```
### default_multi
A second default value is available for multi option, "default_multi". This value is used when we add new value without specified a value.
This "default_multi" must not be a list in multi purpose. For submulti, it has to be a list:
```python
StrOption('shopping_list',
'The shopping list',
['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'],
default_multi='some vegetables',
multi=True)
StrOption('shopping_list',
'The shopping list',
[['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'], ['milk', 'eggs']],
default_multi=['some', 'vegetables'],
multi=submulti)
```
The default_multi value can be a calculation:
```python
def get_a_value():
return 'some vegetables'
StrOption('shopping_list',
'The shopping list',
['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'],
default_multi=Calculation(get_a_value),
multi=True)
```
## Other option's parameters
There are two other parameters.
We will see them later:
- [Property](property.md)
- [Validator](validator.md)

View file

@ -1,35 +0,0 @@
# Generic container: OptionDescription
## Option description's description
First of all, an option description is a name, a description and children.
### name
The "name" is important to retrieve this option description.
### description
The "description" allows the user to understand where this option description will contains.
### children
List of children option.
> **_NOTE:_** the option can be an option or an other option description
Let's try:
```python
from tiramisu import StrOption, OptionDescription
child1 = StrOption('first', 'First basic option')
child2 = StrOption('second', 'Second basic option')
OptionDescription('basic',
'Basic options',
[child1, child2])
```
## Option description's properties
See [property](property.md).

View file

@ -1,501 +0,0 @@
# Default Options
Basic options
## Textual option: StrOption
Option that accept any textual data in Tiramisu:
```python
from tiramisu import StrOption
StrOption('str', 'str', 'value')
StrOption('str', 'str', '1')
```
Other type generate an error:
```python
try:
StrOption('str', 'str', 1)
except ValueError as err:
print(err)
```
returns:
```
"1" is an invalid string for "str"
```
## Integers option: IntOption
Option that accept any integers number in Tiramisu:
```python
from tiramisu import IntOption
IntOption('int', 'int', 1)
```
### min_number and max_number
This option can also verify minimal and maximal number:
```python
IntOption('int', 'int', 10, min_number=10, max_number=15)
```
```python
try:
IntOption('int', 'int', 16, max_number=15)
except ValueError as err:
print(err)
```
returns:
```
"16" is an invalid integer for "int", value must be less than "15"
```
> **_NOTE:_** If "warnings_only" parameter it set to True, it will only emit a warning.
## Floating point number option: FloatOption
Option that accept any floating point number in Tiramisu:
```python
from tiramisu import FloatOption
FloatOption('float', 'float', 10.1)
```
## Boolean option: BoolOption
Boolean values are the two constant objects False and True:
```python
from tiramisu import BoolOption
BoolOption('bool', 'bool', True)
BoolOption('bool', 'bool', False)
```
## Choice option: ChoiceOption
Option that only accepts a list of possible choices.
For example, we just want allowed 1 or 'see later':
```python
from tiramisu import ChoiceOption
ChoiceOption('choice', 'choice', (1, 'see later'), 1)
ChoiceOption('choice', 'choice', (1, 'see later'), 'see later')
```
Any other value isn't allowed:
```python
try:
ChoiceOption('choice', 'choice', (1, 'see later'), "i don't know")
except ValueError as err:
print(err)
```
returns:
```
"i don't know" is an invalid choice for "choice", only "1" and "see later" are allowed
```
# Network options
## IPv4 address option: IPOption
An Internet Protocol address (IP address) is a numerical label assigned to each device connected to a computer network that uses the Internet Protocol for communication.
This option only support version 4 of the Internet Protocol.
```python
from tiramisu import IPOption
IPOption('ip', 'ip', '192.168.0.24')
```
### private_only
By default IP could be a private or a public address. It's possible to restrict to only private IPv4 address with "private_only" attributs:
```python
try:
IPOption('ip', 'ip', '1.1.1.1', private_only=True)
except ValueError as err:
print(err)
```
returns:
```
"1.1.1.1" is an invalid IP for "ip", must be private IP
```
> **_NOTE:_** If "warnings_only" parameter it set to True, it will only emit a warning.
### allow_reserved
By default, IETF reserved are not allowed. The "allow_reserved" can be used to allow this address:
```python
try:
IPOption('ip', 'ip', '255.255.255.255')
except ValueError as err:
print(err)
```
returns:
```
"255.255.255.255" is an invalid IP for "ip", mustn't be reserved IP
```
But this doesn't raises:
```python
IPOption('ip', 'ip', '255.255.255.255', allow_reserved=True)
```
> **_NOTE:_** If "warnings_only" parameter it set to True, it will only emit a warning.
### cidr
Classless Inter-Domain Routing (CIDR) is a method for allocating IP addresses and IP routing.
CIDR notation, in which an address or routing prefix is written with a suffix indicating the number of bits of the prefix, such as 192.168.0.1/24.
```python
IPOption('ip', 'ip', '192.168.0.1/24', cidr=True)
try:
IPOption('ip', 'ip', '192.168.0.0/24', cidr=True)
except ValueError as err:
print(err)
```
returns:
```
"192.168.0.0/24" is an invalid IP for "ip", it's in fact a network address
```
## Port option: PortOption
A port is a network communication endpoint.
A port is a string object:
```python
from tiramisu import PortOption
PortOption('port', 'port', '80')
```
### allow_range
A range is a list of port where we specified first port and last port number. The separator is ":":
```python
PortOption('port', 'port', '2000', allow_range=True)
PortOption('port', 'port', '2000:3000', allow_range=True)
```
### allow_zero
By default, port 0 is not allowed, if you want allow it, use the parameter "allow_zero":
```python
try:
PortOption('port', 'port', '0')
except ValueError as err:
print(err)
```
returns:
```python
"0" is an invalid port for "port", must be between 1 and 49151
```
But this doesn't raises:
```python
PortOption('port', 'port', '0', allow_zero=True)
```
> **_NOTE:_** This option affect the minimal and maximal port number, if "warnings_only" parameter it set to True, it will only emit a warning.
### allow_wellknown
The well-known ports (also known as system ports) are those from 1 through 1023. This parameter is set to True by default:
```python
PortOption('port', 'port', '80')
```
```python
try:
PortOption('port', 'port', '80', allow_wellknown=False)
except ValueError as err:
print(err)
```
returns:
```
"80" is an invalid port for "port", must be between 1024 and 49151
```
> **_NOTE:_** This option affect the minimal and maximal port number, if "warnings_only" parameter it set to True, it will only emit a warning.
### allow_registred
The registered ports are those from 1024 through 49151. This parameter is set to True by default:
```python
PortOption('port', 'port', '1300')
```
```python
try:
PortOption('port', 'port', '1300', allow_registred=False)
except ValueError as err:
print(err)
```
returns:
```
"1300" is an invalid port for "port", must be between 1 and 1023
```
> **_NOTE:_** This option affect the minimal and maximal port number, if "warnings_only" parameter it set to True, it will only emit a warning.
### allow_protocol
```python
PortOption('port', 'port', '1300', allow_protocol="True")
PortOption('port', 'port', 'tcp:1300', allow_protocol="True")
PortOption('port', 'port', 'udp:1300', allow_protocol="True")
```
### allow_private
The dynamic or private ports are those from 49152 through 65535. One common use for this range is for ephemeral ports. This parameter is set to False by default:
```python
try:
PortOption('port', 'port', '64000')
except ValueError as err:
print(err)
```
returns:
```
"64000" is an invalid port for "port", must be between 1 and 49151
```
```python
PortOption('port', 'port', '64000', allow_private=True)
```
> **_NOTE:_** This option affect the minimal and maximal port number, if "warnings_only" parameter it set to True, it will only emit a warning.
## Subnetwork option: NetworkOption
IP networks may be divided into subnetworks:
```python
from tiramisu import NetworkOption
NetworkOption('net', 'net', '192.168.0.0')
```
### cidr
Classless Inter-Domain Routing (CIDR) is a method for allocating IP addresses and IP routing.
CIDR notation, in which an address or routing prefix is written with a suffix indicating the number of bits of the prefix, such as 192.168.0.0/24:
```python
NetworkOption('net', 'net', '192.168.0.0/24', cidr=True)
```
## Subnetwork mask option: NetmaskOption
For IPv4, a network may also be characterized by its subnet mask or netmask. This option allow you to enter a netmask:
```python
from tiramisu import NetmaskOption
NetmaskOption('mask', 'mask', '255.255.255.0')
```
## Broadcast option: BroadcastOption
The last address within a network broadcast transmission to all hosts on the link. This option allow you to enter a broadcast:
```python
from tiramisu import BroadcastOption
BroadcastOption('bcast', 'bcast', '192.168.0.254')
```
# Internet options
## Domain name option: DomainnameOption
Domain names are used in various networking contexts and for application-specific naming and addressing purposes.
The DomainnameOption allow different type of domain name:
```python
from tiramisu import DomainnameOption
DomainnameOption('domain', 'domain', 'foo.example.net')
DomainnameOption('domain', 'domain', 'foo', type='hostname')
```
### type
There is three type for a domain name:
- "domainname" (default): lowercase, number, "-" and "." characters are allowed, this must have at least one "."
- "hostname": lowercase, number and "-" characters are allowed, the maximum length is 63 characters
- "netbios": lowercase, number and "-" characters are allowed, the maximum length is 15 characters
> **_NOTE:_** If "warnings_only" parameter it set to True, it will raise if length is incorrect by only emit a warning character is not correct.
### allow_ip
If the option can contain a domain name or an IP, you can set the "allow_ip" to True, (False by default):
```python
DomainnameOption('domain', 'domain', 'foo.example.net', allow_ip=True)
DomainnameOption('domain', 'domain', '192.168.0.1', allow_ip=True)
```
In this case, IP is validate has IPOption would do.
### allow_cidr_network
If the option can contain a CIDR network, you can set "allow_cidr_network" to True, (False by default):
```python
DomainnameOption('domain', 'domain', 'foo.example.net', allow_cidr_network=True)
DomainnameOption('domain', 'domain', '192.168.0.0/24', allow_cidr_network=True)
```
### allow_without_dot
A domain name with domainname's type must have a dot. If "allow_without_dot" is True (False by default), we can set a domainname or an hostname:
```python
DomainnameOption('domain', 'domain', 'foo.example.net', allow_without_dot=True)
DomainnameOption('domain', 'domain', 'foo', allow_without_dot=True)
```
### allow_startswith_dot
A domain name with domainname's type mustn't start by a dot. .example.net is not a valid domain. In some case it could be interesting to allow domain name starts by a dot (for ACL in Squid, no proxy option in Firefox, ...):
```python
DomainnameOption('domain', 'domain', 'example.net', allow_startswith_dot=True)
DomainnameOption('domain', 'domain', '.example.net', allow_startswith_dot=True)
```
## MAC address option: MACOption
Validate MAC address:
```python
from tiramisu import MACOption
MACOption('mac', 'mac', '01:10:20:20:10:30')
```
## Uniform Resource Locator option: UrlOption
An Uniform Resource Locator is, in fact, a string starting with http:// or https://, a DomainnameOption, optionaly ':' and a PortOption, and finally filename:
```python
from tiramisu import URLOption
URLOption('url', 'url', 'http://foo.example.fr/index.php')
URLOption('url', 'url', 'https://foo.example.fr:4200/index.php?login=foo&pass=bar')
```
For parameters, see PortOption and DomainnameOption.
## Email address option: EmailOption
Electronic mail (email or e-mail) is a method of exchanging messages ("mail") between people using electronic devices. Here an example:
```python
from tiramisu import EmailOption
EmailOption('mail', 'mail', 'foo@example.net')
```
# Unix options
## Unix username: UsernameOption
An unix username option is a 32 characters maximum length with lowercase ASCII characters, number, '_' or '-'. The username have to start with lowercase ASCII characters or "_":
```python
from tiramisu import UsernameOption
UsernameOption('user', 'user', 'my_user')
```
## Unix groupname: GroupnameOption
Same condition for unix group name:
```python
from tiramisu import GroupnameOption
GroupnameOption('group', 'group', 'my_group')
```
## Password option: PasswordOption
Simple string with no other restriction:
```python
from tiramisu import PasswordOption
PasswordOption('pass', 'pass', 'oP$¨1jiJie')
```
## Unix filename option: FilenameOption
For this option, only lowercase and uppercas ASCII character, "-", ".", "_", "~", and "/" are allowed:
```python
from tiramisu import FilenameOption
FilenameOption('file', 'file', '/etc/tiramisu/tiramisu.conf')
```
## Unix file permissions: PermissionsOption
Valid the representing Unix permissions is an octal (base-8) notation.
```python
from tiramisu import PermissionsOption
PermissionsOption('perms', 'perms', 755)
PermissionsOption('perms', 'perms', 1755)
```
This option doesn't allow (or display a warning with warnings_only):
- 777 (two weak value)
- others have more right than group
- group has more right than user
# Date option
## Date option: DateOption
Date option waits for a date with format YYYY-MM-DD:
```python
from tiramisu import DateOption
DateOption('date', 'date', '2019-10-30')
```

View file

@ -1,183 +0,0 @@
# Create it's own option
## Generic regexp option: "RegexpOption"
Use "RegexpOption" to create custom option is very simple.
You just have to create an object that inherits from "RegexpOption" and that has the following class attributes:
- \_\_slots\_\_: with new data members (the values should always be "tuple()")
- \_type = with a name
- \_display_name: with the display name (for example in error message)
- \_regexp: with a compiled regexp
Here an example to an option that only accept string with on lowercase ASCII vowel characters:
```python
import re
from tiramisu import RegexpOption
class VowelOption(RegexpOption):
__slots__ = tuple()
_type = 'vowel'
_display_name = "string with vowel"
_regexp = re.compile(r"^[aeiouy]*$")
```
Let's try our object:
```python
VowelOption('vowel', 'Vowel', 'aae')
```
```python
try:
VowelOption('vowel', 'Vowel', 'oooups')
except ValueError as err:
print(err)
```
returns:
```python
"oooups" is an invalid string with vowel for "Vowel"
```
## Create a custom option
An option always inherits from "Option" object. This object has the following class attributes:
- \_\_slots\_\_: a tuple with new data members (by default you should set "tuple()")
- \_type = with a name
- \_display_name: with the display name (for example in error message)
Here an example to an lipogram option:
```python
from tiramisu import Option
from tiramisu.error import ValueWarning
import warnings
class LipogramOption(Option):
__slots__ = tuple()
_type = 'lipogram'
_display_name = 'lipogram'
#
# First of all we want to add a custom parameter to ask the minimum length (min_len) of the value:
def __init__(self,
*args,
min_len=100,
**kwargs):
# store extra parameters
extra = {'_min_len': min_len}
super().__init__(*args,
extra=extra,
**kwargs)
#
# We have a first validation method.
# In this method, we verify that the value is a string and that there is no "e" on it:
# Even if user set warnings_only attribute, this method will raise.
def validate(self,
value):
# first, valid that the value is a string
if not isinstance(value, str):
raise ValueError('invalid string')
# and verify that there is any 'e' in the sentense
if 'e' in value:
raise ValueError('Perec wrote a book without any "e", you could not do it in a simple sentence?')
#
# Finally we add a method to valid the value length.
# If "warnings_only" is set to True, a warning will be emit:
def second_level_validation(self,
value,
warnings_only):
# retrive parameter in extra
min_len = self.impl_get_extra('_min_len')
# verify the sentense length
if len(value) < min_len:
# raise message, in this case, warning and error message are different
if warnings_only:
msg = f'it would be better to have at least {min_len} characters in the sentence'
else:
msg = f'you must have at least {min_len} characters in the sentence'
raise ValueError(msg)
```
Let's test it:
1. the character "e" is in the value:
```python
try:
LipogramOption('lipo',
'Lipogram',
'I just want to add a quality string that has no bad characters')
except ValueError as err:
print(err)
```
returns:
```
"I just want to add a quality string that has no bad characters" is an invalid lipogram for "Lipogram", Perec wrote a book without any "e", you could not do it in a simple sentence?
```
2. the character "e" is in the value and warnings_only is set to True:
```python
try:
LipogramOption('lipo',
'Lipogram',
'I just want to add a quality string that has no bad characters',
warnings_only=True)
except ValueError as err:
print(err)
```
```
"I just want to add a quality string that has no bad characters" is an invalid lipogram for "Lipogram", Perec wrote a book without any "e", you could not do it in a simple sentence?
```
3. the value is too short
```python
try:
LipogramOption('lipo',
'Lipogram',
'I just want to add a quality string that has no bad symbols')
except ValueError as err:
print(err)
```
returns:
```
"I just want to add a quality string that has no bad symbols" is an invalid lipogram for "Lipogram", you must have at least 100 characters in the sentence
```
4. the value is too short and warnings_only is set to True:
```python
warnings.simplefilter('always', ValueWarning)
with warnings.catch_warnings(record=True) as warn:
LipogramOption('lipo',
'Lipogram',
'I just want to add a quality string that has no bad symbols',
warnings_only=True)
if warn:
print(warn[0].message)
```
returns:
```
attention, "I just want to add a quality string that has no bad symbols" could be an invalid lipogram for "Lipogram", it would be better to have at least 100 characters in the sentence
```
5. set minimum length to 50 characters, the value is valid:
```python
LipogramOption('lipo',
'Lipogram',
'I just want to add a quality string that has no bad symbols',
min_len=50)
```

View file

@ -1,109 +0,0 @@
# Property
## What are properties ?
The properties are a central element of Tiramisu.
Properties change the behavior of an option or make it unavailable.
## Read only and read write
Config can be in two defaut mode:
### read_only
You can get all variables in a "Config" that not disabled.
Off course, as the Config is read only, you cannot set any value to any option.
Only value with :doc`calculation` can change value.
You cannot access to mandatory variable without values. Verify that all values is set before change mode.
### read_write
You can get all options not disabled and not hidden. You can also set all variables not frozen.
## Common properties
### hidden
Option with this property can only get value in read only mode.
This property is used for option that user cannot modifify it's value (for example if it's value is calculated).
### disabled
We never can access to option with this property.
### frozen
Options with this property cannot be modified.
## Special option properties
### mandatory
You should set value for option with this properties. In read only mode we cannot access to this option if no value is set.
### empty or notempty
Only used for multi option that are not a follower.
Mandatory for a multi means that you cannot add None as a value. But value [] is allowed. This is not permit with "empty" property.
A multi option has automaticly "empty" property. If you don't want allow empty option, just add "notempty" property when you create the option.
### unique or notunique
Only used for multi option that are not a follower.
Raise ValueError if a value is set twice or more in a multi Option.
A multi option has automaticly "unique" property. If you want allow duplication in option, just add "notunique" property when you create the option.
### permissive
Option with 'permissive' cannot raise PropertiesOptionError for properties set in permissive.
Config with 'permissive', whole option in this config cannot raise PropertiesOptionError for properties set in permissive.
## Special Config properties
### cache
Enable cache settings and values.
### expire
Enable settings and values in cache expire after "expiration_time" (by default 5 seconds).
### everything_frozen
Whole options in config are frozen (even if option have not frozen property).
### force_default_on_freeze
Whole options frozen in config will lost his value and use default values.
### force_metaconfig_on_freeze
Whole options frozen in config will lost his value and get MetaConfig values.
### force_store_value
All options with this properties will be mark has modified.
### validator
Launch validator set by user in option (this property has no effect for option validation and second level validation).
### warnings
Display warnings during validation.
### demoting_error_warning
All value's errors are convert to warning (ValueErrorWarning).
## Own properties
There are no specific instructions for creating a property. Just add a string as property create a new property.

View file

@ -1,13 +0,0 @@
# The symbolic link option: SymLinkOption
A "SymLinkOption" is an option that actually points to another option.
Each time we will access to a properties of this options, we will have in return the value of other option.
Creation a "SymLinkOption" is easy:
```python
from tiramisu import StrOption, SymLinkOption
st = StrOption('str', 'str')
sym = SymLinkOption('sym', st)
```

View file

@ -1,494 +0,0 @@
# Validator
## What are validator ?
Validator is a functionnality that allow you to call a function to determine if an option is valid.
To define validator we have to use [calculation](calculation.md) object.
The function have to raise a "ValueError" object if the value is not valid. It could emit a warning when raises a "ValueWarning".
## Validator with options
Here an example, where we want to ask a new password to an user. This password should not be weak. The password will be asked twice and must match.
First of all, import necessary object:
```python
from asyncio import run
import warnings
from re import match
from tiramisu import StrOption, IntOption, OptionDescription, Config, \
Calculation, Params, ParamOption, ParamSelfOption, ParamValue
from tiramisu.error import ValueWarning
```
Create a first function to valid that the password is not weak:
```python
def is_password_conform(password):
# password must containe at least a number, a lowercase letter, an uppercase letter and a symbol
if not match(r'(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*\W)', password):
raise ValueError('please choose a stronger password, try a mix of letters, numbers and symbols')
```
Secondly create a function to valid password length. The password must be longer than the value of "min_len" and should be longer than the value of "recommand_len".
In first case, function raise "ValueError", this value is incorrect.
In second case, function raise "ValueWarning", the value is valid but discouraged:
```python
def password_correct_len(min_len, recommand_len, password):
# password must have at least min_len characters
if len(password) < min_len:
raise ValueError(f'use {min_len} characters or more for your password')
# password should have at least recommand_len characters
if len(password) < recommand_len:
raise ValueWarning(f'it would be better to use more than {recommand_len} characters for your password')
```
Thirdly create a function that verify that the login name is not a part of password (password "foo2aZ$" if not valid for user "foo"):
```python
def user_not_in_password(login, password):
if login in password:
raise ValueError('the login must not be part of the password')
```
Now we can creation an option to ask user login:
```python
login = StrOption('login', 'Login', properties=('mandatory',))
```
Create a calculation to launch "is_password_conform". This function will be use in a new option and must validate this new option. So we use the object "ParamSelfOption" has parameter to retrieve the value of current option:
```python
calc1 = Calculation(is_password_conform,
Params(ParamSelfOption()))
```
Create a second calculation to launch "password_correct_len" function. We want set 8 as "min_len" value and 12 as "recommand_len" value:
```python
calc2 = Calculation(password_correct_len,
Params((ParamValue(8),
ParamValue(12),
ParamSelfOption())))
```
Create a third calculation to launch "user_not_in_password" function. For this function, we use keyword argument. This function normaly raise "ValueError" but in this case we want demoting this error as a simple warning. So we add "warnings_only" parameter:
```python
calc3 = Calculation(user_not_in_password,
Params(kwargs={'login': ParamOption(login),
'password': ParamSelfOption()}),
warnings_only=True)
```
So now we can create first password option that use those calculations:
```python
password1 = StrOption('password1',
'Password',
properties=('mandatory',),
validators=[calc1, calc2, calc3])
```
A new function is created to conform that password1 and password2 match:
```python
def password_match(password1, password2):
if password1 != password2:
raise ValueError("those passwords didn't match, try again")
```
And now we can create second password option that use this function:
```python
password2 = StrOption('password2',
'Confirm',
properties=('mandatory',),
validators=[Calculation(password_match, Params((ParamOption(password1), ParamSelfOption())))])
```
Finally we create optiondescription and config:
```python
od = OptionDescription('password', 'Define your password', [password1, password2])
root = OptionDescription('root', '', [login, od])
async def main():
config = await Config(root)
await config.property.read_write()
run(main())
```
Now we can test this "Config" with a tested password too weak:
```python
async def main():
config = await Config(root)
await config.property.read_write()
await config.option('login').value.set('user')
try:
await config.option('password.password1').value.set('aAbBc')
except ValueError as err:
print(f'Error: {err}')
run(main())
```
returns:
```
Error: "aAbBc" is an invalid string for "Password", please choose a stronger password, try a mix of letters, numbers and symbols
```
The password is part of error message. In this case it's a bad idea. So we have to remove "prefix" to the error message:
```python
async def main():
config = await Config(root)
await config.property.read_write()
await config.option('login').value.set('user')
try:
await config.option('password.password1').value.set('aAbBc')
except ValueError as err:
err.prefix = ''
print(f'Error: {err}')
run(main())
```
Now the error is:
```
Error: please choose a stronger password, try a mix of letters, numbers and symbols
```
Let's try with a password not weak but too short:
```python
async def main():
config = await Config(root)
await config.property.read_write()
await config.option('login').value.set('user')
try:
await config.option('password.password1').value.set('aZ$1')
except ValueError as err:
err.prefix = ''
print(f'Error: {err}')
run(main())
```
The error is:
```
Error: use 8 characters or more for your password
```
Now try a password with 8 characters:
```python
warnings.simplefilter('always', ValueWarning)
async def main():
config = await Config(root)
await config.property.read_write()
await config.option('login').value.set('user')
with warnings.catch_warnings(record=True) as warn:
await config.option('password.password1').value.set('aZ$1bN:2')
if warn:
warn[0].message.prefix = ''
print(f'Warning: {warn[0].message}')
password = await config.option('password.password1').value.get()
print(f'The password is "{password}"')
run(main())
```
Warning is display but password is store:
```
Warning: it would be better to use more than 12 characters for your password
The password is "aZ$1bN:2"
```
Try a password with the login as part of it:
```python
warnings.simplefilter('always', ValueWarning)
async def main():
config = await Config(root)
await config.property.read_write()
await config.option('login').value.set('user')
with warnings.catch_warnings(record=True) as warn:
await config.option('password.password1').value.set('aZ$1bN:2u@1Bjuser')
if warn:
warn[0].message.prefix = ''
print(f'Warning: {warn[0].message}')
password = await config.option('password.password1').value.get()
print(f'The password is "{password}"')
run(main())
```
Warning is display but password is store:
```
Warning: the login must not be part of the password
The password is "aZ$1bN:2u@1Bjuser"
```
Now try with a valid password but that doesn't match:
```python
async def main():
config = await Config(root)
await config.property.read_write()
await config.option('login').value.set('user')
await config.option('password.password1').value.set('aZ$1bN:2u@1Bj')
try:
await config.option('password.password2').value.set('aZ$1aaaa')
except ValueError as err:
err.prefix = ''
print(f'Error: {err}')
run(main())
```
An error is displayed:
```
Error: those passwords didn't match, try again
```
Finally try a valid password:
```python
async def main():
config = await Config(root)
await config.property.read_write()
await config.option('login').value.set('user')
await config.option('login').value.set('user')
await config.option('password.password1').value.set('aZ$1bN:2u@1Bj')
await config.option('password.password2').value.set('aZ$1bN:2u@1Bj')
await config.property.read_only()
user_login = await config.option('login').value.get()
password = await config.option('password.password2').value.get()
print(f'The password for "{user_login}" is "{password}"')
run(main())
```
As expected, we have:
```
The password for "user" is "aZ$1bN:2u@1Bj"
```
## Validator with a multi option
Assume we ask percentage value to an user. The sum of values mustn't be higher than 100% and shouldn't be lower than 100%.
Let's start by importing the objects:
```python
from asyncio import run
import warnings
from tiramisu import IntOption, OptionDescription, Config, \
Calculation, Params, ParamSelfOption
from tiramisu.error import ValueWarning
```
Continue by writing the validation function:
```python
def valid_pourcent(option):
total = sum(option)
if total > 100:
raise ValueError(f'the total {total}% is bigger than 100%')
if total < 100:
raise ValueWarning(f'the total {total}% is lower than 100%')
```
And create a simple option in a single OptionDescription:
```python
percent = IntOption('percent',
'Percent',
multi=True,
validators=[Calculation(valid_pourcent, Params(ParamSelfOption()))])
od = OptionDescription('root', 'root', [percent])
```
Now try with bigger sum:
```python
async def main():
config = await Config(od)
try:
await config.option('percent').value.set([20, 90])
except ValueError as err:
err.prefix = ''
print(f'Error: {err}')
percent_value = await config.option('percent').value.get()
print(f'The value is "{percent_value}"')
run(main())
```
The result is:
```
Error: the total 110% is bigger than 100%
The value is "[]"
```
Let's try with lower sum:
```python
async def main():
config = await Config(od)
warnings.simplefilter('always', ValueWarning)
with warnings.catch_warnings(record=True) as warn:
await config.option('percent').value.set([20, 70])
if warn:
warn[0].message.prefix = ''
print(f'Warning: {warn[0].message}')
percent_value = await config.option('percent').value.get()
print(f'The value is "{percent_value}"')
run(main())
```
The result is:
```
Warning: the total 90% is lower than 100%
The value is "[20, 70]"
```
Finally with correct value:
```python
async def main():
config = await Config(od)
await config.option('percent').value.set([20, 80])
percent_value = await config.option('percent').value.get()
print(f'The value is "{percent_value}"')
run(main())
```
The result is:
```
The value is "[20, 80]"
```
## Validator with a follower option
Assume we want distribute something to differents users. The sum of values mustn't be higher than 100%.
First, import all needed objects:
```python
from asyncio import run
import warnings
from tiramisu import StrOption, IntOption, Leadership, OptionDescription, Config, \
Calculation, Params, ParamSelfOption, ParamIndex
from tiramisu.error import ValueWarning
```
Let's start to write a function with three arguments:
- the first argument will have all values set for the follower
- the second argument will have only last value set for the follower
- the third argument will have the index
```python
def valid_pourcent(option, current_option, index):
if None in option:
return
total = sum(option)
if total > 100:
raise ValueError(f'the value {current_option} (at index {index}) is too big, the total is {total}%')
```
Continue by creating a calculation:
```python
calculation = Calculation(valid_pourcent, Params((ParamSelfOption(whole=True),
ParamSelfOption(),
ParamIndex())))
```
And instanciate differents option:
```python
user = StrOption('user', 'User', multi=True)
percent = IntOption('percent',
'Distribution',
multi=True,
validators=[calculation])
leader = Leadership('percent', 'Percent', [user, percent])
od = OptionDescription('root', 'root', [leader])
```
Add two values to the leader:
```python
async def main():
config = await Config(od)
await config.option('percent.user').value.set(['user1', 'user2'])
run(main())
```
The user user1 will have 20%:
```python
async def main():
config = await Config(od)
await config.option('percent.user').value.set(['user1', 'user2'])
await config.option('percent.percent', 0).value.set(20)
run(main())
```
If we try to set 90% to user2:
```python
async def main():
config = await Config(od)
await config.option('percent.user').value.set(['user1', 'user2'])
await config.option('percent.percent', 0).value.set(20)
try:
await config.option('percent.percent', 1).value.set(90)
except ValueError as err:
err.prefix = ''
print(f'Error: {err}')
run(main())
```
results:
```
Error: the value 90 (at index 1) is too big, the total is 110%
```
No problem with 80%:
```python
async def main():
config = await Config(od)
await config.option('percent.user').value.set(['user1', 'user2'])
await config.option('percent.percent', 0).value.set(20)
await config.option('percent.percent', 1).value.set(80)
run(main())
```

23
docs/Makefile Normal file
View file

@ -0,0 +1,23 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = pyfun
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
mkdir -p _build/html/_modules
make -C ../src all

4
docs/_static/css/custom.css vendored Normal file
View file

@ -0,0 +1,4 @@
.wy-table-responsive table td {
white-space: normal;
}

BIN
docs/_static/python-logo-large.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,93 @@
==================
Global permissive
==================
Permissives allow access, during a calculation, to a normally unavailable variable.
In the :doc:api_property example we add a new `create` option that has a calculation with the option `exists` as parameter.
This option has a calculated default_multi value. If the file exists (so `exists` option is True) we don't want create automaticly the file:
.. literalinclude:: src/api_global_permissive.py
:lines: 7-8
:linenos:
Here is the new option:
.. literalinclude:: src/api_global_permissive.py
:lines: 20-23
:linenos:
:download:`download the config <src/api_global_permissive.py>`
Get/add/pop/reset global permissive
=====================================
Let's try this config:
>>> config.property.read_write()
>>> config.option('new.filename').value.set(['/etc', '/unknown'])
>>> config.value.get()
{'new.filename': ['/etc', '/unknown'], 'new.exists': [True, False], 'new.create': [False, True]}
Now we want to see `advanced` option. But how calculate create value?
>>> config.property.add('advanced')
>>> try:
... config.value.get()
... except ConfigError as err:
... print(err)
unable to carry out a calculation for "Create automaticly the file", cannot access to option "This file exists" because has property "advanced"
We just have to add `advanced` permissive to allow calculation:
>>> config.permissive.add('advanced')
>>> config.value.get()
{'new.filename': ['/etc', '/unknown'], 'new.create': [False, True]}
At any time we can retrieve all global permissive:
>>> config.permissive.get()
frozenset({'hidden', 'advanced'})
We can remove on permissive:
>>> config.permissive.pop('hidden')
>>> config.permissive.get()
frozenset({'advanced'})
And finally we can reset all permissives:
>>> config.permissive.reset()
>>> config.permissive.get()
frozenset()
Default permissives
============================
Tiramisu estimate default permissive.
All properties added in `read write` mode and removed in `read only` mode are, by default, included in permissive list when we change mode:
>>> default = config.property.getdefault('read_write', 'append')
>>> config.property.setdefault(frozenset(default | {'advanced'}), 'read_write', 'append')
>>> default = config.property.getdefault('read_only', 'remove')
>>> config.property.setdefault(frozenset(default | {'advanced'}), 'read_only', 'remove')
>>> config.property.read_write()
>>> config.permissive.get()
frozenset({'advanced', 'hidden'})
Importation and exportation
================================
In config, all permissive (global's and option's permissives) can be exportated:
>>> config.permissive.exportation()
{None: frozenset({'hidden', 'advanced'})}
And reimported later:
>>> export = config.permissive.exportation()
>>> config.permissive.importation(export)
.. note:: The exportation format is not stable and can be change later, please do not use importation otherwise than jointly with exportation.

View file

@ -0,0 +1,185 @@
================
Global property
================
Before start, have a look to :doc:property.
Let's start by import needed objects:
.. literalinclude:: src/api_global_property.py
:lines: 1-4
:linenos:
Instanciate a first option with `mandatory` property:
.. literalinclude:: src/api_global_property.py
:lines: 7-10
:linenos:
Instanciate a second option with calculated value, which verify if the file exists.
This options has `frozen`, `force_default_on_freeze` and `advanced` properties:
.. literalinclude:: src/api_global_property.py
:lines: 11-15
:linenos:
This two options are in a leadership:
.. literalinclude:: src/api_global_property.py
:lines: 16-18
:linenos:
Finally create the root option description and the config:
.. literalinclude:: src/api_global_property.py
:lines: 19-20
:linenos:
:download:`download the config <src/api_global_property.py>`
Read only and read write
==========================
By default, there is no restriction.
For example, it's possible to change value of a `frozen` option (here the `exists`' option):
>>> config.option('new.filename').value.set(['/etc'])
>>> config.option('new.exists', 0).value.set(False)
To have the good properties in "read / write" mode:
>>> config.property.read_write()
>>> config.option('new.filename').value.set(['/etc'])
>>> try:
... config.option('new.exists', 0).value.set(False)
...except PropertiesOptionError as err:
... print(err)
cannot modify the option "This file exists" because has property "frozen"
The read write mode is used be a human who wants to modify the configuration.
Some variables are not displayed, because this person cannot modified it.
To have the good properties in "read only" mode:
>>> config.property.read_only()
>>> try:
... config.option('new.filename').value.set(['/etc'])
...except PropertiesOptionError as err:
... print(err)
cannot modify the option "Filename" because has property "frozen"
In this mode it is impossible to modify the values of the options.
It should be use by a script, for build a template, ...
All variables not desactived are accessible.
Get/add/pop/reset global property
=======================================
The default read only and read write properties are:
>>> config.property.read_only()
>>> config.property.get()
frozenset({'force_store_value', 'validator', 'everything_frozen', 'warnings', 'cache', 'mandatory', 'frozen', 'empty', 'disabled'})
>>> config.property.read_write()
>>> config.property.get()
frozenset({'frozen', 'cache', 'warnings', 'disabled', 'validator', 'force_store_value', 'hidden'})
In the current config, the option has property `advanced`.
Has you can see below, the `advanced` is not used in any mode. This property doesn't affect Tiramisu.
Imagine that you don't want to see any advanced option by default. Just add this property in global property:
>>> config.option('new.filename').value.set(['/etc'])
>>> config.property.read_write()
>>> config.value.get()
{'new.filename': ['/etc'], 'new.exists': [True]}
>>> config.property.add('advanced')
>>> config.property.get()
frozenset({'frozen', 'advanced', 'hidden', 'validator', 'force_store_value', 'disabled', 'cache', 'warnings'})
>>> config.value.get()
{'new.filename': ['/etc']}
Of course you want to access to this option in read only mode.
So you have to remove this property:
>>> config.property.read_only()
>>> config.property.pop('advanced')
>>> config.property.get()
frozenset({'force_store_value', 'everything_frozen', 'frozen', 'warnings', 'empty', 'disabled', 'mandatory', 'cache', 'validator'})
>>> config.value.get()
{'new.filename': ['/etc'], 'new.exists': [True]}
At any time we can return to the default property (default means initialized properties, before change to read only or read write mode):
>>> config.property.read_only()
>>> config.property.get()
frozenset({'empty', 'cache', 'force_store_value', 'everything_frozen', 'warnings', 'frozen', 'disabled', 'mandatory', 'validator'})
>>> config.property.reset()
>>> config.property.get()
frozenset({'cache', 'warnings', 'validator'})
Get default properties in mode
=======================================
Add or pop properties each time we pass from one mode to an other is not a good idea. It better to change `read_write` and `read_only` mode directly.
Change mode means, in fact, add some properties and remove some other properties.
For example, when we pass to read_write mode, this properties are added:
>>> config.property.getdefault('read_write', 'append')
frozenset({'disabled', 'validator', 'force_store_value', 'hidden', 'frozen'})
and this properties are remove:
>>> config.property.getdefault('read_write', 'remove')
frozenset({'empty', 'everything_frozen', 'mandatory', 'permissive'})
Here is properties added when pass to read_only mode:
>>> config.property.getdefault('read_only', 'append')
frozenset({'empty', 'mandatory', 'validator', 'disabled', 'force_store_value', 'everything_frozen', 'frozen'})
and this properties are remove:
>>> config.property.getdefault('read_only', 'remove')
frozenset({'hidden', 'permissive'})
Just add the property to the default value to automatically automate the addition and deletion.
We want to add the property when we switch to "read write" mode and automatically delete this property when we switch to "read only" mode:
>>> default = config.property.getdefault('read_write', 'append')
>>> config.property.setdefault(frozenset(default | {'advanced'}), 'read_write', 'append')
>>> default = config.property.getdefault('read_only', 'remove')
>>> config.property.setdefault(frozenset(default | {'advanced'}), 'read_only', 'remove')
Let's try:
>>> 'advanced' in config.property.get()
False
>>> config.property.read_write()
>>> 'advanced' in config.property.get()
True
>>> config.property.read_only()
>>> 'advanced' in config.property.get()
False
Importation and exportation
=======================================
In config, all properties (global's and option's properties) can be exportated:
>>> config.property.exportation()
{None: frozenset({'empty', 'cache', 'warnings', 'validator', 'disabled', 'force_store_value', 'everything_frozen', 'frozen', 'mandatory'})}
And reimported later:
>>> export = config.property.exportation()
>>> config.property.importation(export)
.. note:: The exportation format is not stable and can be change later, please do not use importation otherwise than jointly with exportation.

View file

@ -0,0 +1,17 @@
=====================
Option's permissive
=====================
.. FIXME advanced in permissive
.. FIXME unrestraint, forcepermissive

View file

@ -0,0 +1,403 @@
==================
Option's property
==================
Let's start to build a config.
First of, import needed object:
.. literalinclude:: src/api_option_property.py
:lines: 1-9
:linenos:
Instanciate a first option to call a file name:
.. literalinclude:: src/api_option_property.py
:lines: 56-59
:linenos:
Secondly add an `exists` option to know if this file is already created:
.. literalinclude:: src/api_option_property.py
:lines: 60-64
:linenos:
Thirdly add a `create` option used by a potential script to create wanted file:
.. literalinclude:: src/api_option_property.py
:lines: 65-72
:linenos:
A new option is create to known the file type. If file already exists, retrieve automaticly the type, otherwise ask to the user:
.. literalinclude:: src/api_option_property.py
:lines: 35-42
:linenos:
.. literalinclude:: src/api_option_property.py
:lines: 73-87
:linenos:
In same model, create a `user` and `group` name options:
.. literalinclude:: src/api_option_property.py
:lines: 12-32
:linenos:
.. literalinclude:: src/api_option_property.py
:lines: 88-111
:linenos:
Finally create a `mode` option:
.. literalinclude:: src/api_option_property.py
:lines: 45-53
:linenos:
.. literalinclude:: src/api_option_property.py
:lines: 112-116
:linenos:
Let's build the config:
.. literalinclude:: src/api_option_property.py
:lines: 118-124
:linenos:
:download:`download the config <src/api_option_property.py>`
Get/add/pop/reset property
=================================
option description's property
'''''''''''''''''''''''''''''''''''''''''
An option description is an option. It's possible to set property to it:
>>> config.property.read_write()
>>> config.option('new').property.get()
set()
To add a property:
>>> config.option('new').property.add('disabled')
>>> config.option('new').property.get()
set('disabled')
The property affect the option description:
>>> try:
... config.option('new').value.get()
... except PropertiesOptionError as err:
... print(err)
cannot access to optiondescription "Add new file" because has property "disabled"
But, of course the child option too. If access to option description is not possible, it's not possible to child option too:
>>> try:
... config.option('new.filename').value.get()
... except PropertiesOptionError as err:
... print(err)
cannot access to optiondescription "Add new file" because has property "disabled"
We can remove an existed property too:
>>> config.option('new').property.add('hidden')
>>> config.option('new').property.get()
{'hidden', 'disabled'}
>>> config.option('new').property.pop('hidden')
>>> config.option('new').property.get()
{'disabled'}
It's possible to reset property:
>>> config.option('new').property.reset()
>>> config.option('new').value.get()
{'filename': [], 'exists': [], 'create': [], 'type': [], 'user': [], 'group': [], 'mode': []}
option's property
'''''''''''''''''''''''''''''''''''''''
In a simple option we can add, pop or reset property:
>>> config.property.read_write()
>>> config.option('new.filename').property.get())
{'mandatory', 'unique', 'empty'}
>>> config.option('new.filename').property.add('frozen')
>>> config.option('new.filename').property.get()
{'mandatory', 'unique', 'frozen', 'empty'}
>>> config.option('new.filename').property.pop('empty')
>>> config.option('new.filename').property.get()
{'frozen', 'mandatory', 'unique'}
>>> config.option('new.filename').property.reset()
>>> config.option('new.filename').property.get()
{'mandatory', 'unique', 'empty'}
leader's property
''''''''''''''''''''''''''''
In leader's option can only have a list of property. For other's property, please set directly in leadership option:
>>> config.property.read_write()
>>> try:
... config.option('new.filename').property.add('hidden')
... except LeadershipError as err:
... print(err)
leader cannot have "hidden" property
>>> config.option('new').property.add('hidden')
This `hidden` property has to affect leader option but also all follower option.
That why you have to set this kind of properties directly in leadership option.
.. note::
Allowed properties for a leader: 'empty', 'unique', 'force_store_value', 'mandatory', 'force_default_on_freeze', 'force_metaconfig_on_freeze', and 'frozen'.
follower's property
'''''''''''''''''''''''''''''''''''''''
First of add, add values in leader option:
>>> config.property.read_write()
>>> config.option('new.filename').value.set(['/etc/passwd', 'unknown1', 'unknown2'])
We have to get property with an index:
>>> config.option('new.create', 1).property.get()
set()
We can set property with index:
>>> config.option('new.create', 1).property.add('frozen')
>>> config.option('new.create', 1).property.get()
{'frozen'}
>>> config.option('new.create', 2).property.get()
set()
But we can alse set without index (available for all follower's value):
>>> config.option('new.create').property.add('frozen')
>>> print(config.option('new.create', 1).property.get())
{'frozen'}
>>> print(config.option('new.create', 2).property.get())
{'frozen'}
Calculated property
=======================
A property can be a :doc:`calculation`. That means that the property will be set or not following the context.
The Calculation can return two type of value:
- a `str` this string is a new property
- `None` so this property is cancel
First of all, have a look to the `create` properties:
.. literalinclude:: src/api_option_property.py
:lines: 68-72
:linenos:
This option has only one property which is `disabled` when `exists` has value True.
If the file exists, we don't have to now if user wants create it. It is already exists. So we don't have to access to this option.
Secondly, have a look to the `type` properties:
.. literalinclude:: src/api_option_property.py
:lines: 79-87
:linenos:
There is:
- two static properties: `force_default_on_freeze` and `mandatory`.
- two calculated properties: `hidden` and `frozen`
If the file is already exists, the two calculated properties are present to this option.
So we can access to this option only in read only mode and user cannot modified it's value.
Finally have a look to the `username` and `grpname` options' properties:
.. literalinclude:: src/api_option_property.py
:lines: 94-99
:linenos:
In this case we have two properties:
- one static property: `force_store_value`
- one calculated property: `mandatory`
This calculated property is apply only if `create` is True.
Be carefull to the `create` option. It could be disabled, so not accessible in calculation if the file exists as see previously.
That why we add notraisepropertyerror attribute to True, even if the calculation will failed.
In this case the value of `create` is not add in `calc_value` argument.
In this case the function `calc_value` consider that the property `mandatory` has to be set.
But we just want to set `mandatory` property only if create is False. That why we add the no_condition_is_invalid to True.
Force the registration of a value
====================================
The property `force_store_value` is a special property. This property permit to store a value automaticly even if user do not set value or reset the value.
This is useful especially, for example, for recording a random draw password through a calculation. Or to store any first result for a calculation.
To the, create a new config:
>>> config = Config(root)
If we add value in `filename`, the option `exists` stay a default value, but not the `mode` option, which has `force_store_value`:
>>> config.property.read_write()
>>> config.option('new.filename').value.set(['/etc'])
>>> print(config.option('new.filename').owner.get())
user
>>> print(config.option('new.exists', 0).owner.get())
default
>>> print(config.option('new.mode', 0).owner.get())
forced
If we try to reset `mode` value, this option is modified:
>>> config.option('new.mode', 0).value.reset()
>>> config.option('new.mode', 0).owner.get()
forced
Non-empty value, mandatory and unique
========================================================
Leader and multi have automaticly two properties `unique` and `empty`:
>>> config = Config(OptionDescription('root', 'root', [FilenameOption('filename',
... 'Filename',
... multi=True)]))
>>> config.option('filename').property.get()
{'empty', 'unique'}
To remove `empty` property
>>> config = Config(OptionDescription('root', 'root', [FilenameOption('filename',
... 'Filename',
... properties=('notempty',),
... multi=True)]))
>>> config.option('filename').property.get()
{'unique'}
>>> config = Config(OptionDescription('root', 'root', [FilenameOption('filename',
... 'Filename',
... properties=('notunique',),
... multi=True)]))
>>> config.option('filename').property.get()
{'empty'}
Let's try with previous config.
First of all we remove `force_store_value` mode:
>>> config = Config(root)
>>> properties = config.property.getdefault('read_write', 'append') - {'force_store_value'}
>>> config.property.setdefault(frozenset(properties), 'read_write', 'append')
>>> properties = config.property.getdefault('read_only', 'append') - {'force_store_value'}
>>> config.property.setdefault(frozenset(properties), 'read_only', 'append')
In addition to the specified `mandatory` property, leader have automaticly two properties: `unique` and `empty`:
>>> config.option('new.filename').property.get()
{'unique', 'mandatory', 'empty'}
What is the difference between the property `unique` and `mandatory`?
Let's try with no value at all:
>>> config.property.read_only()
>>> try:
... config.option('new.filename').value.get()
>>> except PropertiesOptionError as err:
... print(err)
cannot access to option "Filename" because has property "mandatory"
A `mandatory` multi must have at least one value. This value is check only in read only mode.
If we remove the `mandatory` property, the value is valid:
>>> config.property.read_write()
>>> config.option('new.filename').property.pop('mandatory')
>>> config.option('new.filename').property.get()
{'unique', 'empty'}
>>> config.property.read_only()
>>> config.option('new.filename').value.get()
[]
A `empty` multi can has no value, but if you set a value, it must not be None:
>>> config.property.read_write()
>>> config.option('new.filename').value.set(['/etc', None])
>>> config.property.read_only()
>>> try:
... config.option('new.filename').value.get()
... except PropertiesOptionError as err:
... print(err)
cannot access to option "Filename" because has property "empty"
Trying now without this property:
>>> config.property.read_write()
>>> config.option('new.filename').property.pop('empty')
>>> config.option('new.filename').value.set(['/etc', None])
>>> config.property.read_only()
>>> config.option('new.filename').value.get()
['/etc', None]
A `unique` property in multi means you cannot have same value twice:
>>> config.property.read_write()
>>> try:
... config.option('new.filename').value.set(['/etc', '/etc'])
... except ValueError as err:
... print(err)
"['/etc', '/etc']" is an invalid file name for "Filename", the value "/etc" is not unique
When removing this property:
>>> config.property.read_write()
>>> config.option('new.filename').property.pop('unique')
>>> config.option('new.filename').value.set(['/etc', '/etc'])
>>> config.property.read_only()
>>> config.option('new.filename').value.get()
['/etc', '/etc']
Non-modifiable option
=====================
Freeze an option means that you cannot change the value of this option:
>>> config = Config(root)
>>> config.property.read_write()
>>> config.option('new.filename').value.set(['unknown'])
>>> config.option('new.create', 0).value.set(False)
>>> config.option('new.create', 0).property.add('frozen')
>>> try:
... config.option('new.create', 0).value.set(False)
... except PropertiesOptionError as err:
... print(err)
cannot modify the option "Create automaticly the file" because has property "frozen"
Sometime (for example when an option is calculated) we want retrieve the default value (so the calculated value) when we add `frozen` option.
In the current example, `new.exists` is a calculated value and we don't want that the used modify this option. So we add `frozen` and `force_default_on_freeze` properties.
For example, without mode, we can modify the `new.exists` option, but in `read_only` mode, we want to have default value:
>>> config = Config(root)
>>> config.option('new.filename').value.set(['unknown'])
>>> config.option('new.exists', 0).value.set(True)
>>> config.option('new.exists', 0).value.get()
True
>>> config.property.read_write()
>>> config.option('new.exists', 0).value.get()
False
The property `force_default_on_freeze` is also avalaible in the option `new.type`. If the file exists, the type is calculated but if it not already exists, the user needs to set the correct wanted type.

31
docs/api_property.rst Normal file
View file

@ -0,0 +1,31 @@
==================================
Playing with property
==================================
Properties and permissives affect the Tiramisu behaviour.
This mechanism makes available or not other options. It also controls the behavior of options.
.. toctree::
:maxdepth: 2
api_global_properties
api_global_permissives
api_option_property
api_option_permissive
.. class TiramisuContextValue(TiramisuConfig):
.. def mandatory(self):
.. """Return path of options with mandatory property without any value"""
..
.. class TiramisuOptionPermissive(CommonTiramisuOption):
.. """Manage option's permissive"""
.. def get(self):
.. """Get permissives value"""
.. def set(self, permissives):
.. """Set permissives value"""
.. def reset(self):
.. """Reset all personalised permissive"""

View file

@ -1,8 +1,12 @@
# Manage values
==================================
Manage values
==================================
## Values with options
Values with options
=========================
### Simple option
Simple option
----------------------------
Begin by creating a Config. This Config will contains two options:
@ -11,129 +15,92 @@ Begin by creating a Config. This Config will contains two options:
Let's import needed object:
```python
from asyncio import run
from shutil import disk_usage
from os.path import isdir
from tiramisu import FilenameOption, FloatOption, OptionDescription, Config, \
Calculation, Params, ParamValue, ParamOption, ParamSelfOption
```
.. literalinclude:: src/api_value.py
:lines: 1-4
:linenos:
Create a function that verify the path exists in current system:
```python
def valid_is_dir(path):
# verify if path is a directory
if not isdir(path):
raise ValueError('this directory does not exist')
```
.. literalinclude:: src/api_value.py
:lines: 6-9
:linenos:
Use this function as a :doc:`validator` in a new option call `path`:
```python
filename = FilenameOption('path', 'Path', validators=[Calculation(valid_is_dir,
Params(ParamSelfOption()))])
```
.. literalinclude:: src/api_value.py
:lines: 24-25
:linenos:
Create a second function that calculate the disk usage:
```python
def calc_disk_usage(path, size='bytes'):
# do not calc if path is None
if path is None:
return None
if size == 'bytes':
div = 1
else:
# bytes to gigabytes
div = 1024 * 1024 * 1024
return disk_usage(path).free / div
```
.. literalinclude:: src/api_value.py
:lines: 11-21
:linenos:
Add a new option call `usage` that use this function with first argument the option `path` created before:
```python
usage = FloatOption('usage', 'Disk usage', Calculation(calc_disk_usage,
Params(ParamOption(filename))))
```
.. literalinclude:: src/api_value.py
:lines: 26-27
:linenos:
Finally add those options in option description and a Config:
```python
disk = OptionDescription('disk', 'Verify disk usage', [filename, usage])
root = OptionDescription('root', 'root', [disk])
async def main():
config = await Config(root)
await config.property.read_write()
return config
.. literalinclude:: src/api_value.py
:lines: 28-31
:linenos:
config = run(main())
```
:download:`download the config <src/api_value.py>`
#### Get and set a value
Get and set a value
'''''''''''''''''''''''''''''
First of all, retrieve the values of both options:
```python
async def main():
print(await config.option('disk.path').value.get())
print(await config.option('disk.usage').value.get())
run(main())
```
returns:
```
>>> config.option('disk.path').value.get()
None
>>> config.option('disk.usage').value.get()
None
```
Enter a value of the `path` option:
```python
async def main():
await config.option('disk.path').value.set('/')
print(await config.option('disk.path').value.get())
print(await config.option('disk.usage').value.get())
>>> config.option('disk.path').value.set('/')
run(main())
```
The value is really change:
returns:
```
>>> config.option('disk.path').value.get()
/
Now, calculation retrieve a value:
>>> config.option('disk.usage').value.get()
668520882176.0
```
When you enter a value it is validated:
```python
async def main():
try:
await config.option('disk.path').value.set('/unknown')
except ValueError as err:
print(err)
run(main())
```
returns:
```
>>> try:
>>> config.option('disk.path').value.set('/unknown')
>>> except ValueError as err:
>>> print(err)
"/unknown" is an invalid file name for "Path", this directory does not exist
```
#### Is value is valid?
We can also set a :doc:`calculation` as value. For example, we want to launch previous function but with in_gb to True as second argument:
>>> calc = Calculation(calc_disk_usage, Params((ParamOption(filename),
... ParamValue('gigabytes'))))
>>> config.option('disk.usage').value.set(calc)
>>> config.option('disk.usage').value.get()
622.6080360412598
Is value is valid?
'''''''''''''''''''''''''''''
To check is a value is valid:
```python
await config.option('disk.path').value.valid()
```
>>> config.option('disk.path').value.valid()
True
#### Display the default value
Display the default value
'''''''''''''''''''''''''''''
Even if the value is modify, you can display the default value with `default` method:
@ -144,7 +111,8 @@ Even if the value is modify, you can display the default value with `default` me
>>> config.option('disk.usage').value.default()
668510105600.0
#### Return to the default value
Return to the default value
'''''''''''''''''''''''''''''
If the value is modified, just `reset` it to retrieve the default value:
@ -155,7 +123,8 @@ If the value is modified, just `reset` it to retrieve the default value:
>>> config.option('disk.path').value.get()
None
#### The ownership of a value
The ownership of a value
'''''''''''''''''''''''''''''
Every option has an owner, that will indicate who changed the option's value last.
@ -206,26 +175,28 @@ We can change this owner:
>>> config.option('disk.path').owner.get()
itsme
### Get choices from a Choice option
Get choices from a Choice option
--------------------------------------
In the previous example, it's difficult to change the second argument of the `calc_disk_usage`.
For ease the change, add a `ChoiceOption` and replace the `size_type` and `disk` option:
.. literalinclude:: ../src/api_value_choice.py
.. literalinclude:: src/api_value_choice.py
:lines: 26-31
:linenos:
We set the default value to `bytes`, if not, the default value will be None.
:download:`download the config <../src/api_value_choice.py>`
:download:`download the config <src/api_value_choice.py>`
At any time, we can get all de choices avalaible for an option:
>>> config.option('disk.size_type').value.list()
('bytes', 'giga bytes')
### Value in multi option
Value in multi option
--------------------------------------
.. FIXME undefined
@ -237,25 +208,26 @@ First of all, we have to modification in this option:
- add multi attribute to True
- the function use in validation valid a single value, so each value in the list must be validate separatly, for that we add whole attribute to False in `ParamSelfOption` object
.. literalinclude:: ../src/api_value_multi.py
.. literalinclude:: src/api_value_multi.py
:lines: 23-25
:linenos:
Secondly, the function calc_disk_usage must return a list:
.. literalinclude:: ../src/api_value_multi.py
.. literalinclude:: src/api_value_multi.py
:lines: 11-26
:linenos:
Finally `usage` option is also a multi:
.. literalinclude:: ../src/api_value_multi.py
.. literalinclude:: src/api_value_multi.py
:lines: 27-30
:linenos:
:download:`download the config <../src/api_value_multi.py>`
:download:`download the config <src/api_value_multi.py>`
#### Get or set a multi value
Get or set a multi value
'''''''''''''''''''''''''''''
Since the options are multi, the default value is a list:
@ -272,7 +244,8 @@ A multi option waiting for a list:
>>> config.option('disk.usage').value.get()
[668499898368.0, 8279277568.0]
#### The ownership of multi option
The ownership of multi option
'''''''''''''''''''''''''''''
There is no difference in behavior between a simple option and a multi option:
@ -285,7 +258,8 @@ default
>>> config.option('disk.path').owner.get()
user
### Leadership
Leadership
--------------------------------------
In previous example, we cannot define different `size_type` for each path. If you want do this, you need a leadership.
@ -295,23 +269,24 @@ As each value of followers are isolate, the function `calc_disk_usage` will rece
So let's change this function:
.. literalinclude:: ../src/api_value_leader.py
.. literalinclude:: src/api_value_leader.py
:lines: 12-18
:linenos:
Secondly the option `size_type` became a multi:
.. literalinclude:: ../src/api_value_leader.py
.. literalinclude:: src/api_value_leader.py
:lines: 24-25
:linenos:
Finally disk has to be a leadership:
.. literalinclude:: ../src/api_value_leader.py
.. literalinclude:: src/api_value_leader.py
:lines: 30
:linenos:
#### Get and set a leader
Get and set a leader
'''''''''''''''''''''''''''''
A leader is, in fact, a multi option:
@ -346,7 +321,8 @@ To reduce use the `pop` method:
>>> config.option('disk.path').value.get()
['/']
#### Get and set a follower
Get and set a follower
'''''''''''''''''''''''''''''
As followers are isolate, we cannot get all the follower values:
@ -376,7 +352,8 @@ As the leader, follower has a length (in fact, this is the leader's length):
>>> config.option('disk.size_type').value.len()
2
#### The ownership of a leader and follower
The ownership of a leader and follower
'''''''''''''''''''''''''''''''''''''''''''
There is no differences between a multi option and a leader option:
@ -396,37 +373,36 @@ True
>>> config.option('disk.size_type', 1).owner.get()
default
## Values in option description
Values in option description
==============================
With an option description we can have directly a dict with all option's name and value:
>>> config.option('disk.path').value.set(['/', '/tmp'])
>>> config.option('disk.size_type', 0).value.set('giga bytes')
>>> config.option('disk').value.dict()
{'path': ['/', '/tmp'], 'size_type': ['giga bytes', 'bytes'], 'usage': [622.578239440918, 8279273472.0]}
An attribute fullpath permit to have fullpath of child option:
>>> config.option('disk').value.dict(fullpath=True)
>>> config.option('disk').value.get()
{'disk.path': ['/', '/tmp'], 'disk.size_type': ['giga bytes', 'bytes'], 'disk.usage': [622.578239440918, 8279273472.0]}
## Values in config
Values in config
==========================
###dict
get
--------
With the `config` we can have directly a dict with all option's name and value:
>>> config.option('disk.path').value.set(['/', '/tmp'])
>>> config.option('disk.size_type', 0).value.set('giga bytes')
>>> config.value.dict()
>>> config.value.get()
{'disk.path': ['/', '/tmp'], 'disk.size_type': ['giga bytes', 'bytes'], 'disk.usage': [622.578239440918, 8279273472.0]}
If you don't wan't path but only the name:
>>> config.value.dict(flatten=True)
>>> config.value.get(flatten=True)
{'path': ['/', '/tmp'], 'size_type': ['giga bytes', 'bytes'], 'usage': [622.578239440918, 8279273472.0]}
### importation/exportation
importation/exportation
------------------------
In config, we can export full values:
@ -439,4 +415,3 @@ and reimport it later:
>>> config.value.importation(export)
.. note:: The exportation format is not stable and can be change later, please do not use importation otherwise than jointly with exportation.

119
docs/application.rst Normal file
View file

@ -0,0 +1,119 @@
==================================
A full application
==================================
The firefox network configuration
-------------------------------------
Now we are going to resume everything we have seen with a concrete example.
We're going to take an example based on the `Mozilla Firefox
<https://www.mozilla.org/en-US/firefox/>`_ proxy's
configuration, like what is required when you open the `network settings` in
the General configuration's firefox page:
.. image:: images/firefox_preferences.png
The tiramisu's configuration
----------------------------
Build the `Config`
''''''''''''''''''''
First, let's create our options :
.. literalinclude:: src/application.py
:lines: 1-3, 12-20
:linenos:
This first option is the most important one : its value will determine which other options
are disabled and which are not. The same thing will happen with other options later.
Here are the others options we'll be using :
.. literalinclude:: src/application.py
:lines: 23-221
:linenos:
As you can see, we're using :doc:`value <api_value>`, :doc:`property <api_option_property>`
and :doc:`calculation` in the setting of our options, because we have many options which
value or accessibility is depending on the value of other options.
Now we need to create OptionDescriptions and configs :
.. literalinclude:: src/application.py
:lines: 223-232
:linenos:
Download the :download:`full code <src/application.py>` of this example.
As you can see, we regrouped a lot of options in 'protocols', so we can set a calculated `disabled` property
that is apply to all those options. This way, we don't have to put the instruction on every option
one by one.
Let's try
'''''''''''''''
Now that we have our Config, it's time to run some tests !
Here are a few code blocks you can test and the results you should get :
1. Automatic proxy configuration URL:
>>> proxy_config.property.read_write()
>>> proxy_config.option('proxy_mode').value.set('Automatic proxy configuration URL')
>>> proxy_config.option('auto_config_url').value.set('http://192.168.1.1/wpad.dat')
>>> proxy_config.property.read_only()
>>> for path, value in proxy_config.value.get().items():
... print(proxy_config.option(path).option.doc() + ': "' + str(value) + '"')
Proxy's config mode: "Automatic proxy configuration URL"
Address for which proxy will be desactivated: "[]"
Proxy's auto config URL: "http://192.168.1.1/wpad.dat"
Prompt for authentication if password is saved: "False"
Enable DNS over HTTPS: "False"
2. Auto-detect proxy settings for this network:
>>> proxy_config.property.read_write()
>>> proxy_config.option('proxy_mode').value.set('Auto-detect proxy settings for this network')
>>> proxy_config.option('no_proxy').value.set(['localhost',
... '127.0.0.1',
... '192.16.10.150',
... '192.168.5.101',
... '192.168.56.101/32',
... '192.168.20.0/24',
... '.tiramisu.org',
... 'mozilla.org'])
>>> proxy_config.option('dns_over_https.enable_dns_over_https').value.set(True)
>>> proxy_config.option('dns_over_https.used_dns').value.set('default')
>>> proxy_config.property.read_only()
>>> for path, value in proxy_config.value.get().items():
... print(proxy_config.option(path).option.doc() + ': "' + str(value) + '"')
Proxy's config mode: "Auto-detect proxy settings for this network"
Address for which proxy will be desactivated: "['localhost', '127.0.0.1', '192.16.10.150', '192.168.5.101', '192.168.56.101/32', '192.168.20.0/24', '.tiramisu.org', 'mozilla.org']"
Prompt for authentication if password is saved: "False"
Enable DNS over HTTPS: "True"
Used DNS: "default"
Set use_for_all_protocols to True:
>>> proxy_config.property.read_write()
>>> proxy_config.option('protocols.use_for_all_protocols').value.set(True)
>>> proxy_config.property.read_only()
>>> for path, value in proxy_config.value.get().items():
... print(proxy_config.option(path).option.doc() + ': "' + str(value) + '"')
Proxy's config mode: "Manual proxy configuration"
Address: "192.168.20.1"
Port: "8080"
Use HTTP IP and Port for all protocols: "True"
Address: "192.168.20.1"
Port: "8080"
Address: "192.168.20.1"
Port: "8080"
Address: "192.168.20.1"
Port: "8080"
SOCKS host version used by proxy: "v5"
Address for which proxy will be desactivated: "[]"
Prompt for authentication if password is saved: "False"
Use Proxy DNS when using SOCKS v5: "False"
Enable DNS over HTTPS: "True"
Used DNS: "custom"
Custom DNS URL: "https://dns-url.com"

159
docs/browse.rst Normal file
View file

@ -0,0 +1,159 @@
Browse the :class:`Config`
===========================
Getting the options
----------------------
.. note:: The :class:`Config` object we are using is located here in this script:
:download:`download the source <src/property.py>`
Let's retrieve the config object, named `cfg`
.. code-block:: python
from property import cfg
We retrieve by path an option named `var1`
and then we retrieve its name and its docstring
.. code-block:: bash
:emphasize-lines: 2, 5, 8
print(cfg.option('od1.var1'))
<tiramisu.api.TiramisuOption object at 0x7f3876cc5940>
print(cfg.option('od1.var1').option.name())
'var1'
print(cfg.option('od1.var1').option.doc())
'first option'
Accessing the values of the options
-------------------------------------
Let's browse the configuration structure and option values.
You have getters as a `get` method on option objects:
.. code-block:: bash
:emphasize-lines: 10, 14
# getting all the options
print(cfg.option.value.get())
{'var1': None, 'var2': 'value'}
# getting the `od1` option description
print(cfg.option('od1').value.get())
{'od1.var1': None, 'od1.var2': 'value'}
# getting the var1 option's value
print(cfg.option('od1.var1').value.get())
None
# getting the var2 option's default value
print(cfg.option('od1.var2').value.get())
'value'
# trying to get a non existent option's value
cfg.option('od1.idontexist').value.get()
AttributeError: unknown option "idontexist" in optiondescription "od1"
Setting the value of an option
------------------------------
An important part of the setting's configuration consists of setting the
value's option.
You have setters as a `set` method on option objects.
And if you wanna come back to a default value, use the `reset()` method.
.. code-block:: bash
:emphasize-lines: 2
# changing the `od1.var1` value
cfg.option('od1.var1').value.set('éééé')
print(cfg.option('od1.var1').value.get())
'éééé'
# carefull to the type of the value to be set
cfg.option('od1.var1').value.set(23454)
ValueError: "23454" is an invalid string for "first variable"
# let's come back to the default value
cfg.option('od1.var2').value.reset()
print(cfg.option('od1.var2').value.get())
'value'
.. important:: If the config is `read only`, setting an option's value isn't allowed, see :doc:`property`
Let's make the protocol of accessing a `Config`'s option explicit
(because explicit is better than implicit):
1. If the option has not been declared, an `Error` is raised,
2. If an option is declared, but neither a value nor a default value has
been set, the returned value is `None`,
3. If an option is declared and a default value has been set, but no value
has been set, the returned value is the default value of the option,
4. If an option is declared, and a value has been set, the returned value is
the value of the option.
But there are special exceptions. We will see later on that an option can be a
:term:`mandatory option`. A mandatory option is an option that must have a value
defined.
Searching for an option
~~~~~~~~~~~~~~~~~~~~~~~~~~
In an application, knowing the path of an option is not always feasible.
That's why a tree of options can easily be searched with the :func:`find()` method.
Let's find an option by it's name
And let's find first an option by it's name
The search can be performed in a subtree
.. code-block:: bash
:emphasize-lines: 1, 6, 19
print(cfg.option.find(name='var1'))
# [<tiramisu.api.TiramisuOption object at 0x7f490a530f98>, <tiramisu.api.TiramisuOption object at 0x7f490a530748>]
# If the option name is unique, the search can be stopped once one matched option
# has been found:
print(cfg.option.find(name='var1', first=True))
# <tiramisu.api.TiramisuOption object at 0x7f6c2beae128>
# a search object behaves like a cfg object, for example
print(cfg.option.find(name='var1', first=True).option.name())
# 'var1'
print(cfg.option.find(name='var1', first=True).option.doc())
# a search can be made with various criteria
print(cfg.option.find(name='var3', value=undefined))
print(cfg.option.find(name='var3', type=StrOption))
# the find method can be used in subconfigs
print(cfg.option('od2').find('var1'))
:download:`download the config used for the find <src/find.py>`
The `get` flattening utility
-------------------------------------
In a config or a subconfig, you can print a dict-like representation
.. code-block:: bash
:emphasize-lines: 2
# get the `od1` option description
print(cfg.option('od1').value.get())
{'od1.var1': 'éééé', 'od1.var2': 'value'}

129
docs/calculation.rst Normal file
View file

@ -0,0 +1,129 @@
==================================
Calculation
==================================
Calculation is a generic object that allow you to call an external function.
Simple calculation
==================================
It's structure is the following :
.. literalinclude:: src/calculation.py
:lines: 1-5
:linenos:
Positional and keyword arguments
=========================================
Function's arguments are also specified in this object.
Let's see with a positional argument and a keyword argument:
.. literalinclude:: src/calculation.py
:lines: 8-10
:linenos:
The function `a_function_with_parameters` will be call with the positional argument `value1` to `my value 1` and the keyword argument `value2` to `my value 2`.
So when this function will be executed, it will return `my value 1 my value 2`.
Let's see with two positional arguments:
.. literalinclude:: src/calculation.py
:lines: 13-15
:linenos:
As we have several positional arguments, the first Params' argument is a tuple.
This example will return strictly same result has previous example.
Option has an argument
=========================================
In previous examples, we use ParamValue arguments, which could contain random value. But this value is static and cannot be change.
It could be interesting to use an existant option has an argument:
.. literalinclude:: src/calculation.py
:lines: 18-21
:linenos:
As long as option1 is at its default value, the function will return `1`. If we set option1 to `12`, the function will return `12`.
Pay attention to the properties when you use an option.
This example will raise a ConfigError:
.. literalinclude:: src/calculation.py
:lines: 24-27
:linenos:
It's up to you to define the desired behavior.
If you want the option to be transitively disabled just set the raisepropertyerror argument to True:
.. literalinclude:: src/calculation.py
:lines: 29-31
:linenos:
If you want to remove option in argument, just set the notraisepropertyerror argument to True:
.. literalinclude:: src/calculation.py
:lines: 33-35
:linenos:
In this case, option1 will not pass to function. You have to set a default value to this argument.
So, function will return `None`.
In these examples, the function only accesses to the value of the option. But no additional information is given.
It is possible to add the parameter `todict` to `True` to have the description of the option in addition to its value.
.. literalinclude:: src/calculation.py
:lines: 37-39
:linenos:
This function will return `the option first option has value 1`.
Multi option has an argument
=========================================
An option could be a multi. Here is an example:
.. literalinclude:: src/calculation.py
:lines: 46-49
:linenos:
In this case the function will return the complete list. So `[1]` in this example.
Leader or follower option has an argument
============================================
An option could be a leader:
.. literalinclude:: src/calculation.py
:lines: 51-57
:linenos:
If the calculation is used in a standard multi, it will return `[1]`.
If the calculation is used in a follower, it will return `1`.
An option could be a follower:
.. literalinclude:: src/calculation.py
:lines: 59-65
:linenos:
If the calculation is used in a standard multi, it will return `[2]`.
If the calculation is used in a follower, it will return `2`.
If the calculation is used in a follower we can also retrieve the actual follower index:
.. literalinclude:: src/calculation.py
:lines: 67-73
:linenos:
Context has an argument
=========================================
It is possible to recover a copy of the context directly in a function. On the other hand, the use of the context in a function is a slow action which will haunt the performances. Use it only in case of necessity:
.. literalinclude:: src/calculation.py
:lines: 42-44
:linenos:

718
docs/cmdline_parser.rst Normal file
View file

@ -0,0 +1,718 @@
.. .. default-role:: code
..
.. ==========================
.. Tiramisu-cmdline-parser
.. ==========================
..
..
.. This tutorial is intended to be a gentle introduction to **Tiramisu
.. command-line parser**, a command-line parsing module that comes included with
.. the **Tiramisu**'s library.
..
.. .. note:: There are a lot of other modules that fulfill the same task,
.. namely getopt (an equivalent for getopt() from the C language) and
.. argparse, from the python standard library.
..
.. `tiramisu-cmdline-parser` enables us to *validate* the command line,
.. wich is a quite different scope -- much more powerfull. It is a
.. superset of the argparse_ module
..
.. .. _argparse: https://docs.python.org/3/howto/argparse.html
..
.. What is Tiramisu-cmdline-parser ?
.. ==================================
..
.. Tiramisu-cmdline-parser is a free project that turns Tiramisu's Config into a command line interface.
..
.. It automatically generates arguments, help and usage messages. Tiramisu (or
.. Tiramisu-API) validates all arguments provided by the command line's user.
..
.. Tiramisu-cmdline-parser uses the well known argparse_ module and adds
.. functionnalities upon it.
..
..
.. Installation
.. ==============
..
.. The best way is to use the python pip_ installer
..
.. .. _pip: https://pip.pypa.io/en/stable/installing/
..
.. And then type:
..
.. .. code-block:: bash
..
.. pip install tiramisu-cmdline-parser
..
.. Build a Tiramisu-cmdline-parser
.. =================================
..
.. Lets show the sort of functionality that we are going to explore in this
.. introductory tutorial.
..
.. We are going to start with a simple example, like making a proxy's
.. configuration script.
..
.. First we are going to build the corresponding `Tiramisu` config object:
..
.. .. literalinclude:: src/proxy.py
.. :lines: 1-44
.. :linenos:
.. :name: Proxy1
..
.. Then we invopque the command line parsing library by creating a commandline
.. parser, and we give the configuration's object to it:
..
.. .. literalinclude:: src/proxy.py
.. :lines: 46-48
.. :linenos:
.. :name: Proxy2
..
.. Finally pretty printing the configuration:
..
.. .. literalinclude:: src/proxy.py
.. :lines: 50-51
.. :linenos:
.. :name: Proxy3
..
.. Let's display the help:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py -h
.. usage: proxy.py [-h] [--dns_over_https] [--no-dns_over_https]
.. {No proxy,Manual proxy configuration,Automatic proxy
.. configuration URL}
..
.. positional arguments:
.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL}
.. Proxy's config mode
..
.. optional arguments:
.. -h, --help show this help message and exit
.. --dns_over_https Enable DNS over HTTPS
.. --no-dns_over_https
..
.. Positional argument
.. ======================
..
.. First of all, we have to set the positional argument :option:`proxy_mode`.
..
.. .. option:: proxy_mode
..
.. As it's a `ChoiceOption`, you only have three choices:
..
.. - No proxy
.. - Manual proxy configuration
.. - Automatic proxy configuration URL
..
.. Set proxy_mode to `No proxy`:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "No proxy"
.. {'dns_over_https': False,
.. 'proxy_mode': 'No proxy'}
..
.. Requirements
.. ================
..
.. Disabled options are not visible as arguments in the command line.
.. Those parameters appears or disappears following the context:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "No proxy" -h
.. usage: proxy.py "No proxy" [-h] [--dns_over_https] [--no-dns_over_https]
.. {No proxy,Manual proxy configuration,Automatic
.. proxy configuration URL}
..
.. positional arguments:
.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL}
.. Proxy's config mode
..
.. optional arguments:
.. -h, --help show this help message and exit
.. --dns_over_https Enable DNS over HTTPS
.. --no-dns_over_https
..
.. If proxy_mode is set to "Automatic proxy configuration URL", some new options are visible:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "Automatic proxy configuration URL" -h
.. usage: proxy.py "Automatic proxy configuration URL" [-h] -i AUTO_CONFIG_URL
.. [--no_proxy.no_proxy_network.no_proxy_network [NO_PROXY_NETWORK [NO_PROXY_NETWORK ...]]]
.. [--no_proxy.no_proxy_network.pop-no_proxy_network INDEX]
.. --no_proxy.no_proxy_network.no_proxy_netmask
.. INDEX NO_PROXY_NETMASK
.. [--no_proxy.no_proxy_domain [NO_PROXY_DOMAIN [NO_PROXY_DOMAIN ...]]]
.. [--dns_over_https]
.. [--no-dns_over_https]
.. {No proxy,Manual proxy
.. configuration,Automatic
.. proxy configuration URL}
..
.. positional arguments:
.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL}
.. Proxy's config mode
..
.. optional arguments:
.. -h, --help show this help message and exit
.. --dns_over_https Enable DNS over HTTPS
.. --no-dns_over_https
..
.. configuration.automatic_proxy:
.. Automatic proxy setting
..
.. -i AUTO_CONFIG_URL, --configuration.automatic_proxy.auto_config_url AUTO_CONFIG_URL
.. Proxy's auto config URL
..
.. no_proxy:
.. Disabled proxy
..
.. --no_proxy.no_proxy_domain [NO_PROXY_DOMAIN [NO_PROXY_DOMAIN ...]]
.. Domain names for which proxy will be desactivated
..
.. no_proxy.no_proxy_network:
.. Network for which proxy will be desactivated
..
.. --no_proxy.no_proxy_network.no_proxy_network [NO_PROXY_NETWORK [NO_PROXY_NETWORK ...]]
.. Network addresses
.. --no_proxy.no_proxy_network.pop-no_proxy_network INDEX
.. --no_proxy.no_proxy_network.no_proxy_netmask INDEX NO_PROXY_NETMASK
.. Netmask addresses
..
.. Arguments
.. ===========
..
.. Each option creates an argument. To change the value of this option, just launch the application with the appropriate argument:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "Manual proxy configuration" \
.. --configuration.manual_proxy.http_ip_address 192.168.1.1
.. {'configuration.manual_proxy.http_ip_address': '192.168.1.1',
.. 'configuration.manual_proxy.http_port': '8080',
.. 'configuration.manual_proxy.i': '192.168.1.1',
.. 'configuration.manual_proxy.p': '8080',
.. 'dns_over_https': False,
.. 'no_proxy.no_proxy_domain': [],
.. 'no_proxy.no_proxy_network.no_proxy_netmask': [],
.. 'no_proxy.no_proxy_network.no_proxy_network': [],
.. 'proxy_mode': 'Manual proxy configuration'}
..
.. Fullpath argument or named argument
.. =====================================
..
.. By default, arguments are build with fullpath of option.
.. The `option http_ip_address` is in `manual_proxy` optiondescription, which is also in configuration optiondescription.
.. So the argument is :option:`--configuration.manual_proxy.http_ip_address`:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "Manual proxy configuration" \
.. --configuration.manual_proxy.http_ip_address 192.168.1.1
.. {'configuration.manual_proxy.http_ip_address': '192.168.1.1',
.. 'configuration.manual_proxy.http_port': '8080',
.. 'configuration.manual_proxy.i': '192.168.1.1',
.. 'configuration.manual_proxy.p': '8080',
.. 'dns_over_https': False,
.. 'no_proxy.no_proxy_domain': [],
.. 'no_proxy.no_proxy_network.no_proxy_netmask': [],
.. 'no_proxy.no_proxy_network.no_proxy_network': [],
.. 'proxy_mode': 'Manual proxy configuration'}
..
.. If we set fullpath to `False`:
..
.. .. code-block:: python
..
.. parser = TiramisuCmdlineParser(proxy_config, fullpath=False)
..
.. Arguments are build with the name of the option.
.. The option :option:`http_ip_address` is now :option`--http_ip_address`:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "Manual proxy configuration" \
.. --http_ip_address 192.168.1.1
.. {'configuration.manual_proxy.http_ip_address': '192.168.1.1',
.. 'configuration.manual_proxy.http_port': '8080',
.. 'configuration.manual_proxy.i': '192.168.1.1',
.. 'configuration.manual_proxy.p': '8080',
.. 'dns_over_https': False,
.. 'no_proxy.no_proxy_domain': [],
.. 'no_proxy.no_proxy_network.no_proxy_netmask': [],
.. 'no_proxy.no_proxy_network.no_proxy_network': [],
.. 'proxy_mode': 'Manual proxy configuration'}
..
.. Short argument
.. ===============
..
.. To have short argument, you just have to make `SymLinkOption` to this option:
..
.. .. literalinclude:: src/proxy.py
.. :lines: 10-11
.. :linenos:
.. :name: Proxy4
..
.. Now argument `-i` or `--configuration.manual_proxy.http_ip_address` can be used alternatively:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "Manual proxy configuration" \
.. --configuration.manual_proxy.http_ip_address 192.168.1.1
.. {'configuration.manual_proxy.http_ip_address': '192.168.1.1',
.. 'configuration.manual_proxy.http_port': '8080',
.. 'configuration.manual_proxy.i': '192.168.1.1',
.. 'configuration.manual_proxy.p': '8080',
.. 'dns_over_https': False,
.. 'no_proxy.no_proxy_domain': [],
.. 'no_proxy.no_proxy_network.no_proxy_netmask': [],
.. 'no_proxy.no_proxy_network.no_proxy_network': [],
.. 'proxy_mode': 'Manual proxy configuration'}
..
..
.. $ python3 src/proxy.py "Manual proxy configuration" \
.. -i 192.168.1.1
.. {'configuration.manual_proxy.http_ip_address': '192.168.1.1',
.. 'configuration.manual_proxy.http_port': '8080',
.. 'configuration.manual_proxy.i': '192.168.1.1',
.. 'configuration.manual_proxy.p': '8080',
.. 'dns_over_https': False,
.. 'no_proxy.no_proxy_domain': [],
.. 'no_proxy.no_proxy_network.no_proxy_netmask': [],
.. 'no_proxy.no_proxy_network.no_proxy_network': [],
.. 'proxy_mode': 'Manual proxy configuration'}
..
.. Be carefull, short argument have to be uniqe in the whole configuration.
..
.. Here `-i` argument is define a second time in same Config:
..
.. .. literalinclude:: src/proxy.py
.. :lines: 17-18
.. :linenos:
.. :name: Proxy5
..
.. But `http_ip_address` and `auto_config_url` are not accessible together:
..
.. - `http_ip_address` is visible only if `proxy_mode` is "Manual proxy configuration"
.. - `auto_config_url` is only visible when `proxy_mode` is "Automatic proxy configuration URL"
..
.. Boolean argument
.. ===================
..
.. Boolean option creates two arguments:
..
.. - --<boolean_name>: it activates (set to True) the option
.. - --no-<boolean_name>: it deactivates (set to False) the option
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "No proxy" \
.. --dns_over_https
.. {'dns_over_https': True,
.. 'proxy_mode': 'No proxy'}
..
.. $ python3 src/proxy.py "No proxy" \
.. --no-dns_over_https
.. {'dns_over_https': False,
.. 'proxy_mode': 'No proxy'}
..
.. Multi
.. =========
..
.. Some values are multi. So we can set several value for this option.
..
.. For example, we can set serveral domain (cadoles.com and gnu.org) to "Domain names for which proxy will be desactivated" option:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "Automatic proxy configuration URL" \
.. --configuration.automatic_proxy.auto_config_url http://proxy.cadoles.com/proxy.pac \
.. --no_proxy.no_proxy_domain cadoles.com gnu.org
.. {'configuration.automatic_proxy.auto_config_url': 'http://proxy.cadoles.com/proxy.pac',
.. 'configuration.automatic_proxy.i': 'http://proxy.cadoles.com/proxy.pac',
.. 'dns_over_https': False,
.. 'no_proxy.no_proxy_domain': ['cadoles.com', 'gnu.org'],
.. 'no_proxy.no_proxy_network.no_proxy_netmask': [],
.. 'no_proxy.no_proxy_network.no_proxy_network': [],
.. 'proxy_mode': 'Automatic proxy configuration URL'}
..
.. Leadership
.. ============
..
.. Leadership option are also supported. The leader option is a standard multi option.
.. But follower option are not view as a multi option. Follower value are separate and we need to set index to set a follower option.
..
.. If we want to had two "Network for which proxy will be desactivated":
..
.. - 192.168.1.1/255.255.255.255
.. - 192.168.0.0/255.255.255.0
..
.. We have to do:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "Automatic proxy configuration URL" \
.. --configuration.automatic_proxy.auto_config_url http://proxy.cadoles.com/proxy.pac \
.. --no_proxy.no_proxy_network.no_proxy_network 192.168.1.1 192.168.0.0 \
.. --no_proxy.no_proxy_network.no_proxy_netmask 0 255.255.255.255 \
.. --no_proxy.no_proxy_network.no_proxy_netmask 1 255.255.255.0
.. {'configuration.automatic_proxy.auto_config_url': 'http://proxy.cadoles.com/proxy.pac',
.. 'configuration.automatic_proxy.i': 'http://proxy.cadoles.com/proxy.pac',
.. 'dns_over_https': False,
.. 'no_proxy.no_proxy_domain': [],
.. 'no_proxy.no_proxy_network.no_proxy_netmask': ['255.255.255.255',
.. '255.255.255.0'],
.. 'no_proxy.no_proxy_network.no_proxy_network': ['192.168.1.1', '192.168.0.0'],
.. 'proxy_mode': 'Automatic proxy configuration URL'}
..
.. We cannot reduce leader lenght:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "Automatic proxy configuration URL" \
.. --configuration.automatic_proxy.auto_config_url http://proxy.cadoles.com/proxy.pac \
.. --no_proxy.no_proxy_network.no_proxy_network 192.168.1.1 192.168.0.0 \
.. --no_proxy.no_proxy_network.no_proxy_netmask 0 255.255.255.255 \
.. --no_proxy.no_proxy_network.no_proxy_netmask 1 255.255.255.0 \
.. --no_proxy.no_proxy_network.no_proxy_network 192.168.1.1
.. usage: proxy.py -i "http://proxy.cadoles.com/proxy.pac" --no_proxy.no_proxy_network.no_proxy_network "192.168.1.1" "192.168.0.0" "Automatic proxy configuration URL"
.. [-h] -i AUTO_CONFIG_URL
.. [--no_proxy.no_proxy_network.no_proxy_network [NO_PROXY_NETWORK [NO_PROXY_NETWORK ...]]]
.. [--no_proxy.no_proxy_network.pop-no_proxy_network INDEX]
.. --no_proxy.no_proxy_network.no_proxy_netmask INDEX NO_PROXY_NETMASK
.. [--no_proxy.no_proxy_domain [NO_PROXY_DOMAIN [NO_PROXY_DOMAIN ...]]]
.. [--dns_over_https] [--no-dns_over_https]
.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL}
.. proxy.py: error: cannot reduce length of the leader "--no_proxy.no_proxy_network.no_proxy_network"
..
.. So an argument --pop-<leader> is automatically created. You need to specified index as parameter:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "Automatic proxy configuration URL" \
.. --configuration.automatic_proxy.auto_config_url http://proxy.cadoles.com/proxy.pac \
.. --no_proxy.no_proxy_network.no_proxy_network 192.168.1.1 192.168.0.0 \
.. --no_proxy.no_proxy_network.no_proxy_netmask 0 255.255.255.255 \
.. --no_proxy.no_proxy_network.pop-no_proxy_network 1
.. {'configuration.automatic_proxy.auto_config_url': 'http://proxy.cadoles.com/proxy.pac',
.. 'configuration.automatic_proxy.i': 'http://proxy.cadoles.com/proxy.pac',
.. 'dns_over_https': False,
.. 'no_proxy.no_proxy_domain': [],
.. 'no_proxy.no_proxy_network.no_proxy_netmask': ['255.255.255.255'],
.. 'no_proxy.no_proxy_network.no_proxy_network': ['192.168.1.1'],
.. 'proxy_mode': 'Automatic proxy configuration URL'}
..
.. Validation
.. ===============
..
.. All arguments are validated successively by argparser and Tiramisu:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "Automatic proxy configuration URL" \
.. --configuration.automatic_proxy.auto_config_url cadoles.com
.. usage: proxy.py "Automatic proxy configuration URL" [-h] -i AUTO_CONFIG_URL
.. [--no_proxy.no_proxy_network.no_proxy_network [NO_PROXY_NETWORK [NO_PROXY_NETWORK ...]]]
.. [--no_proxy.no_proxy_network.pop-no_proxy_network INDEX]
.. --no_proxy.no_proxy_network.no_proxy_netmask
.. INDEX NO_PROXY_NETMASK
.. [--no_proxy.no_proxy_domain [NO_PROXY_DOMAIN [NO_PROXY_DOMAIN ...]]]
.. [--dns_over_https]
.. [--no-dns_over_https]
.. {No proxy,Manual proxy
.. configuration,Automatic
.. proxy configuration URL}
.. proxy.py: error: "cadoles.com" is an invalid URL for "Proxys auto config URL", must start with http:// or https://
..
.. In error message, we have the option description ("Proxy's auto config URL") by default.
..
.. That why we redefined display_name function:
..
.. .. literalinclude:: src/proxy.py
.. :lines: 40-43
.. :linenos:
.. :name: Proxy6
..
.. Now we have --<path> as description:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "Automatic proxy configuration URL" \
.. --configuration.automatic_proxy.auto_config_url cadoles.com
.. usage: proxy.py "Automatic proxy configuration URL" [-h] -i AUTO_CONFIG_URL
.. [--no_proxy.no_proxy_network.no_proxy_network [NO_PROXY_NETWORK [NO_PROXY_NETWORK ...]]]
.. [--no_proxy.no_proxy_network.pop-no_proxy_network INDEX]
.. --no_proxy.no_proxy_network.no_proxy_netmask
.. INDEX NO_PROXY_NETMASK
.. [--no_proxy.no_proxy_domain [NO_PROXY_DOMAIN [NO_PROXY_DOMAIN ...]]]
.. [--dns_over_https]
.. [--no-dns_over_https]
.. {No proxy,Manual proxy
.. configuration,Automatic
.. proxy configuration URL}
.. proxy.py: error: "cadoles.com" is an invalid URL for "--configuration.automatic_proxy.auto_config_url", must start with http:// or https://
..
.. Mandatory
.. =============
..
.. Obviously the mandatory options are checked.
..
.. The positional argument is mandatory, so if we don't set it, an error occured:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py
.. usage: proxy.py [-h] [--dns_over_https] [--no-dns_over_https]
.. {No proxy,Manual proxy configuration,Automatic proxy
.. configuration URL}
.. proxy.py: error: the following arguments are required: proxy_mode
..
.. Others arguments are also check:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "Automatic proxy configuration URL"
.. usage: proxy.py "Automatic proxy configuration URL" [-h] -i AUTO_CONFIG_URL
.. [--no_proxy.no_proxy_network.no_proxy_network [NO_PROXY_NETWORK [NO_PROXY_NETWORK ...]]]
.. [--no_proxy.no_proxy_network.pop-no_proxy_network INDEX]
.. --no_proxy.no_proxy_network.no_proxy_netmask
.. INDEX NO_PROXY_NETMASK
.. [--no_proxy.no_proxy_domain [NO_PROXY_DOMAIN [NO_PROXY_DOMAIN ...]]]
.. [--dns_over_https]
.. [--no-dns_over_https]
.. {No proxy,Manual proxy
.. configuration,Automatic
.. proxy configuration URL}
.. proxy.py: error: the following arguments are required: --configuration.automatic_proxy.auto_config_url
..
.. Persistence configuration and mandatories validation
.. ======================================================
..
.. First of all, activate persistence configuration and remove mandatory validation:
..
.. .. literalinclude:: src/proxy_persistent.py
.. :lines: 43-46
.. :linenos:
.. :name: Proxy7
..
.. We can disabled mandatory validation in parse_args function.
..
.. .. literalinclude:: src/proxy_persistent.py
.. :lines: 51
.. :linenos:
.. :name: Proxy8
..
.. In this case, we can store incomplete value:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy_persistent.py 'Manual proxy configuration'
.. {'configuration.manual_proxy.http_ip_address': None,
.. 'configuration.manual_proxy.http_port': '8080',
.. 'configuration.manual_proxy.i': None,
.. 'configuration.manual_proxy.p': '8080',
.. 'dns_over_https': False,
.. 'no_proxy.no_proxy_domain': [],
.. 'no_proxy.no_proxy_network.no_proxy_netmask': [],
.. 'no_proxy.no_proxy_network.no_proxy_network': [],
.. 'proxy_mode': 'Manual proxy configuration'}
..
.. We can complete configuration after:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy_persistent.py -i 192.168.1.1
.. {'configuration.manual_proxy.http_ip_address': '192.168.1.1',
.. 'configuration.manual_proxy.http_port': '8080',
.. 'configuration.manual_proxy.i': '192.168.1.1',
.. 'configuration.manual_proxy.p': '8080',
.. 'dns_over_https': False,
.. 'no_proxy.no_proxy_domain': [],
.. 'no_proxy.no_proxy_network.no_proxy_netmask': [],
.. 'no_proxy.no_proxy_network.no_proxy_network': [],
.. 'proxy_mode': 'Manual proxy configuration'}
..
.. When configuration is already set, help command, display already set options is usage ligne.
..
.. .. code-block:: bash
..
.. $ python3 src/proxy_persistent.py -h
.. usage: proxy_persistent.py -i "192.168.1.1" "Manual proxy configuration"
.. [..]
..
.. Description and epilog
.. ============================
..
.. As argparser, description and epilog message can be added to the generated help:
..
.. .. code-block:: python
..
.. parser = TiramisuCmdlineParser(proxy_config, description='New description!', epilog='New epilog!')
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py -h
.. usage: proxy.py [-h] [--dns_over_https] [--no-dns_over_https]
.. {No proxy,Manual proxy configuration,Automatic proxy
.. configuration URL}
..
.. New description!
..
.. positional arguments:
.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL}
.. Proxy's config mode
..
.. optional arguments:
.. -h, --help show this help message and exit
.. --dns_over_https Enable DNS over HTTPS
.. --no-dns_over_https
..
.. New epilog!
..
..
.. By default, TiramisuCmdlineParser objects line-wrap the description and epilog texts in command-line help messages.
..
.. If there are line breaks in description or epilog, it automatically replace by a space. You need to change formatter class:
..
.. .. code-block:: python
..
.. from argparse import RawDescriptionHelpFormatter
.. parser = TiramisuCmdlineParser(proxy_config, description='New description!\nLine breaks', epilog='New epilog!\nLine breaks', formatter_class=RawDescriptionHelpFormatter)
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py -h
.. usage: proxy.py [-h] [--dns_over_https] [--no-dns_over_https]
.. {No proxy,Manual proxy configuration,Automatic proxy
.. configuration URL}
..
.. New description!
.. Line breaks
..
.. positional arguments:
.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL}
.. Proxy's config mode
..
.. optional arguments:
.. -h, --help show this help message and exit
.. --dns_over_https Enable DNS over HTTPS
.. --no-dns_over_https
..
.. New epilog!
.. Line breaks
..
.. Hide empty optiondescription
.. ===============================
..
.. An empty optiondescription, is an optiondescription without any option (could have others optiondescriptions).
..
.. For example, configuration is an empty optiondescription:
..
.. .. literalinclude:: src/proxy.py
.. :lines: 23-24
.. :linenos:
.. :name: Proxy9
..
.. This optiondescription doesn't appears in help:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "No proxy" -h
.. usage: proxy.py "No proxy" [-h] [--dns_over_https] [--no-dns_over_https]
.. {No proxy,Manual proxy configuration,Automatic
.. proxy configuration URL}
..
.. positional arguments:
.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL}
.. Proxy's config mode
..
.. optional arguments:
.. -h, --help show this help message and exit
.. --dns_over_https Enable DNS over HTTPS
.. --no-dns_over_https
..
..
..
.. This behavior is, in fact, due to two conditions:
..
.. - there is no option
.. - there is no description (None)
..
.. If we add description:
..
.. .. code-block:: python
..
.. configuration = OptionDescription('configuration', 'Configuration',
.. [manual_proxy, automatic_proxy])
..
.. This optiondescription is specified in help:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py "No proxy" -h
.. usage: proxy.py "No proxy" [-h] [--dns_over_https] [--no-dns_over_https]
.. {No proxy,Manual proxy configuration,Automatic
.. proxy configuration URL}
..
.. positional arguments:
.. {No proxy,Manual proxy configuration,Automatic proxy configuration URL}
.. Proxy's config mode
..
.. optional arguments:
.. -h, --help show this help message and exit
.. --dns_over_https Enable DNS over HTTPS
.. --no-dns_over_https
..
.. configuration:
.. Configuration
..
.. If you don't want empty optiondescription even if there is a description, you could add remove_empty_od to True in parse_args function:
..
.. .. code-block:: python
..
.. parser = TiramisuCmdlineParser(proxy_config, remove_empty_od=True)
..
.. SubConfig
.. ================
..
.. Entire Config is transformed into an argument by default.
..
.. It could be interesting to display only 'configuration' OptionDescription.
..
.. To do this, we have to define default all mandatories options outside this scope:
..
.. .. code-block:: python
..
.. proxy_mode = ChoiceOption('proxy_mode', 'Proxy\'s config mode', ('No proxy',
.. 'Manual proxy configuration',
.. 'Automatic proxy configuration URL'),
.. default='Manual proxy configuration',
.. properties=('positional', 'mandatory'))
..
.. Finally specified the root argument to `TiramisuCmdlineParser`:
..
.. .. code-block:: python
..
.. parser = TiramisuCmdlineParser(proxy_config, root='configuration')
..
.. Now, only sub option of configuration is proposed:
..
.. .. code-block:: bash
..
.. $ python3 src/proxy.py -h
.. usage: proxy.py [-h] -i HTTP_IP_ADDRESS -p [HTTP_PORT]
..
.. optional arguments:
.. -h, --help show this help message and exit
..
.. configuration.manual_proxy:
.. Manual proxy settings
..
.. -i HTTP_IP_ADDRESS, --configuration.manual_proxy.http_ip_address HTTP_IP_ADDRESS
.. Proxy's HTTP IP
.. -p [HTTP_PORT], --configuration.manual_proxy.http_port [HTTP_PORT]
.. Proxy's HTTP Port
..

149
docs/conf.py Normal file
View file

@ -0,0 +1,149 @@
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'Tiramisu'
copyright = '2011-2023, Silique'
author = 'egarette'
# The short X.Y version
version = ''
# The full version, including alpha/beta/rc tags
release = '4.0'
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.extlinks', 'sphinx_lesson',
#'myst_parser', 'sphinx.ext.extlinks'
]
#
#myst_enable_extensions = [
# "amsmath",
# "attrs_inline",
# "colon_fence",
# "deflist",
# "dollarmath",
# "fieldlist",
# "html_admonition",
# "html_image",
## "linkify",
# "replacements",
# "smartquotes",
# "strikethrough",
# "substitution",
# "tasklist",
#]
# **extlinks** 'sphinx.ext.extlinks',
# enables syntax like :proxy:`my source <hello>` in the src files
extlinks = {'proxy': ('/proxy/%s.html',
'external link: ')}
default_role = "code"
html_theme = "sphinx_rtd_theme"
pygments_style = 'sphinx'
html_short_title = "Tiramisu"
html_title = "Tiramisu documenation"
# If true, links to the reST sources are added to the pages.
html_show_sourcelink = False
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
html_show_sphinx = False
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
html_show_copyright = True
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
#source_suffix = ['.rst', '.md']
source_suffix = '.rst'
#source_suffix = {
# '.rst': 'restructuredtext',
# '.txt': 'restructuredtext',
# '.md': 'markdown',
#}
# The master toctree document.
master_doc = 'index'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'en'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
#html_theme = 'alabaster'
# **themes**
#html_theme = 'bizstyle'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
def setup(app):
app.add_css_file('css/custom.css')

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

157
docs/config.rst Normal file
View file

@ -0,0 +1,157 @@
The :class:`Config`
====================
Tiramisu is made of almost three main classes/concepts :
- the :class:`Option` stands for the option types
- the :class:`OptionDescription` is the schema, the option's structure
- the :class:`Config` which is the whole configuration entry point
.. image:: config.png
The handling of options
~~~~~~~~~~~~~~~~~~~~~~~~~~
The handling of options is split into two parts: the description of
which options are available, what their possible values and defaults are
and how they are organized into a tree. A specific choice of options is
bundled into a configuration object which has a reference to its option
description (and therefore makes sure that the configuration values
adhere to the option description).
.. toctree::
:maxdepth: 2
option
options
symlinkoption
own_option
Option description are nested Options
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The :class:`Option` (in this case the :class:`BoolOption`),
are organized into a tree into nested
:class:`~tiramisu.option.OptionDescription` objects.
Every option has a name, as does every option group.
.. toctree::
:maxdepth: 2
optiondescription
dynoptiondescription
leadership
Config
~~~~~~
Let's perform a *Getting started* code review :
.. literalinclude:: src/getting_started.py
:lines: 1-12
:linenos:
:name: GettingStarted
Let's review the code. First, line 7, we create an :class:`OptionDescription` named `optgroup`.
.. literalinclude:: src/getting_started.py
:lines: 4, 6-7
:emphasize-lines: 3
Option objects can be created in different ways, here we create a
:class:`BoolOption`
.. literalinclude:: src/getting_started.py
:lines: 4, 8-9
:emphasize-lines: 3
Then, line 12, we make a :class:`Config` with the :class:`OptionDescription` we
built :
.. literalinclude:: src/getting_started.py
:lines: 3, 12
:emphasize-lines: 2
Here is how to print our :class:`Config` details:
.. literalinclude:: src/getting_started.py
:lines: 15
.. code-block:: bash
Root config object that enables us to handle the configuration options
Commands:
description Get option description
dict Convert config and option to tiramisu format
get Get Tiramisu option
has_dependency Test if option has dependency
isdynamic Test if option is a dynamic optiondescription
isleadership Test if option is a leader or a follower
isoptiondescription Test if option is an optiondescription
list List options (by default list only option)
name Get option name
option Select an option by path
path Get option path
type Get de option type
updates Updates value with tiramisu format
Then let's print our :class:`Option` details.
.. literalinclude:: src/getting_started.py
:lines: 17
.. code-block:: bash
Manage selected option
Commands:
dependencies Get dependencies from this option
description Get option description
dict Convert config and option to tiramisu format
extra Get de option extra
followers Get the followers option for a leadership
get Get Tiramisu option
group_type Get type for an optiondescription (only for optiondescription)
has_dependency Test if option has dependency
identifiers Get identifiers for dynamic option
index Get index of option
isdynamic Test if option is a dynamic optiondescription
isfollower Test if option is a follower
isleader Test if option is a leader
isleadership Test if option is a leader or a follower
ismulti Test if option could have multi value
isoptiondescription Test if option is an optiondescription
issubmulti Test if option could have submulti value
issymlinkoption Test if option is a symlink option
leader Get the leader option for a leadership or a follower option
list List options inside an option description (by default list only option)
name Get option name
option For OptionDescription get sub option, for symlinkoption get the linked option
path Get option path
pattern Get the option pattern
type Get de option type
updates Updates value with tiramisu format
Finaly, let's print the :class:`Config`.
.. literalinclude:: src/getting_started.py
:lines: 19
.. code-block:: bash
<Config path=None>
:download:`download the getting started code <src/getting_started.py>`
Go futher with `Option` and `Config`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. toctree::
:maxdepth: 2
property
validator
calculation

View file

@ -0,0 +1,39 @@
@import url("bizstyle.css");
/* a button for the download links */
a.download{
margin-top:15px;
max-width:190px;
background-color:#eee;
border-color:#888888;
color:#333;
display:inline-block;
vertical-align:middle;
text-align:center;
text-decoration:none;
align-items:flex-start;
cursor:default;
-webkit-appearence: push-button;
border-style: solid;
border-width: 1px;
border-radius: 5px;
font-size: 1em;
font-family: inherit;
border-color: #000;
padding-left: 5px;
padding-right: 5px;
width: 100%;
min-height: 30px;
}
/* the bash output looks like different */
div.highlight-bash {
background-color:#eee;
border-style: solid;
border-width: 1px;
border-radius: 5px;
border-color: #000;
padding-left: 5px;
padding-right: 5px;
}

4
docs/custom/theme.conf Normal file
View file

@ -0,0 +1,4 @@
[theme]
inherit = bizstyle
stylesheet = custom.css

View file

@ -0,0 +1,65 @@
==========================================================
Dynamic option description: :class:`DynOptionDescription`
==========================================================
Dynamic option description
==============================================
Dynamic option description is an :class:`OptionDescription` which multiplies according to the return of a function.
.. list-table::
:widths: 15 45
:header-rows: 1
* - Parameter
- Comments
* - name
- The `name` is important to retrieve this option.
* - doc
- The `description` allows the user to understand where this option will be used for.
* - children
- The list of children (Option) include inside.
Note:: the option can be an :doc:`option` or an other option description
* - identifiers
- Identifiers is a :doc:`calculation` that return the list of identifiers used to create dynamic option description.
* - properties
- A list of :doc:`property` (inside a frozenset().
* - informations
- We can add default informations to this option description.
* - group_type
- Type for this group.
Example
==============
Let's try:
>>> from tiramisu import StrOption, DynOptionDescription, Calculation
>>> def return_identifiers():
... return ['1', '2']
>>> child1 = StrOption('first', 'First basic option ')
>>> child2 = StrOption('second', 'Second basic option ')
>>> DynOptionDescription('basic ',
... 'Basic options ',
... [child1, child2],
... Calculation(return_identifiers))
This example will construct:
- Basic options 1:
- First basic option 1
- Second basic option 1
- Basic options 2:
- First basic option 2
- Second basic option 2

View file

@ -1,6 +1,9 @@
# Getting started
==================================
Getting started
==================================
## What is options handling ?
What is options handling?
=================================
Due to more and more available options required to set up an operating system,
compiler options or whatever, it became quite annoying to hand the necessary
@ -9,7 +12,8 @@ options.
To circumvent these problems the configuration control was introduced.
## What is Tiramisu ?
What is Tiramisu?
===================
Tiramisu is an options handler and an options controller, which aims at
producing flexible and fast options access. The main advantages are its access
@ -19,24 +23,35 @@ There is of course type and structure validations, but also
validations towards the whole options. Furthermore, options can be reached and
changed according to the access rules from nearly everywhere.
### Installation
Installation
-------------
The best way is to use the python [pip](https://pip.pypa.io/en/stable/installing/) installer
The best way is to use the python pip_ installer
.. _pip: https://pip.pypa.io/en/stable/installing/
And then type:
```bash
$ pip install tiramisu
```
.. code-block:: bash
### Advanced users
pip install tiramisu
Advanced users
==============
.. _gettingtiramisu:
- the library's development homepage is there_
.. _there: https://forge.cloud.silique.fr/stove/tiramisu/
To obtain a copy of the sources, check it out from the repository using `git`.
We suggest using `git` if one wants to access to the current developments.
```bash
$ git clone https://framagit.org/tiramisu/tiramisu.git
```
.. code-block:: bash
git clone https://forge.cloud.silique.fr/stove/tiramisu.git
This will get you a fresh checkout of the code repository in a local directory
named "tiramisu".
named ``tiramisu``.

84
docs/glossary.rst Normal file
View file

@ -0,0 +1,84 @@
.. default-role:: literal
Glossary
==========
.. glossary::
configuration
Global configuration object, wich contains the whole configuration
options *and* their descriptions (option types and group)
schema
option description
see :class:`tiramisu.option.OptionDescription`
The schema of a configuration :
- the option types
- how they are organised in groups or even subgroups, that's why we
call them **groups** too.
configuration option
An option object wich has a name and a value and can be accessed
from the configuration object
access rules
Global access rules are : :meth:`~config.CommonConfig.read_write()` or
:meth:`~config.Config.read_only()`
default value
Default value of a configuration option. The default value can be
set at instanciation time, or even at any moment. Remember that if
you reset the default value, the owner reset to `default`
freeze
A whole configuration can be frozen (used in read only access).
A single option can be frozen too.
value owner
When an option is modified, including at the instanciation, we
always know who has modified it. It's the owner of the option.
properties
an option with properties is a option wich has property like 'hidden' or 'disabled' is an option
wich has restricted acces rules.
hidden option
a hidden option has a different behaviour on regards to the access
of the value in the configuration.
disabled option
a disabled option has a different behaviour on regards to the access
of the value in the configuration.
mandatory option
A mandatory option is a configuration option wich value has to be
set, that is the default value cannot be `None`.
consistency
Preserving the consistency in a whole configuration is a tricky thing,
tiramisu takes care of it for you.
context
The context is a :class:`tiramisu.setting.Setting()` object in the
configuration that enables us to access to the global properties
for example the `read_write` or `read_only` :term:`access rules`

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View file

@ -1,36 +1,49 @@
![Logo Tiramisu](../logo.png "logo Tiramisu")
.. default-role:: literal
# Python3 Tiramisu library user documentation
.. meta::
## The tasting of `Tiramisu` --- `user documentation`
:description: python tiramisu library user documentation
:keywords: python, tiramisu, tutorial
Tiramisu:
.. title:: Tiramisu
- is a cool, refreshing Italian dessert,
- it is also an [options controller tool](http://en.wikipedia.org/wiki/Configuration_management#Overview)
The tasting of `Tiramisu` --- `user documentation`
===================================================
It's a pretty small, local (that is, straight on the operating system) options handler and controller.
.. image:: logo.png
:height: 150px
`Tiramisu`
- is a cool, refreshing Italian dessert,
- it is also an `options controller tool`_.
.. _`options controller tool`: http://en.wikipedia.org/wiki/Configuration_management#Overview
It's a pretty small, local (that is, straight on the operating system) options
handler and controller.
- [Getting started](gettingstarted.md)
- [The Config](config.md)
- [Browse the Config](browse.md)
- [Manage values](api_value.md)
.. toctree::
:maxdepth: 2
gettingstarted
config
browse
api_value
api_property
storage
application
quiz
glossary
External project:
.. toctree::
:maxdepth: 2
cmdline_parser
.. External project:
..
.. .. toctree::
.. :maxdepth: 2
..
.. cmdline_parser
.. FIXME ca veut rien dire : "AssertionError: type <class 'tiramisu.autolib.Calculation'> invalide pour des propriétés pour protocols, doit être un frozenset"

51
docs/leadership.rst Normal file
View file

@ -0,0 +1,51 @@
==========================================================
Leadership OptionDescription: :class:`Leadership`
==========================================================
A leadership is a special `OptionDescription` that wait a leader and one or multiple followers.
Leader and follower are multi option. The difference is that the length is defined by the length of the option leader.
If the length of leader is 3, all followers have also length 3.
An other different is that the follower is isolate. That means that you can only change on value on a specified index in the list of it's values.
If a value is mark as modified in a specified index, that not affect the other values in other index.
The leadership
==============================================
.. list-table::
:widths: 15 45
:header-rows: 1
* - Parameter
- Comments
* - name
- The `name` is important to retrieve this option.
* - doc
- The `description` allows the user to understand where this option will be used for.
* - children
- The list of children (Option) include inside.
Note:: the option has to be multi or submulti option and not other option description.
* - properties
- A list of :doc:`property` (inside a frozenset().
* - informations
- We can add default informations to this option description.
Example
====================
Let's try:
>>> from tiramisu import StrOption, Leadership
>>> users = StrOption('users', 'User', multi=True)
>>> passwords = StrOption('passwords', 'Password', multi=True)
>>> Leadership('users',
... 'User allow to connect',
... [users, passwods])

BIN
docs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

139
docs/option.rst Normal file
View file

@ -0,0 +1,139 @@
==================================
Instanciate an option
==================================
Option
========
.. list-table::
:widths: 15 45
:header-rows: 1
* - Parameter
- Comments
* - name
- The `name` is important to retrieve this option.
* - doc
- The `description` allows the user to understand where this option will be used for.
* - default
- For each option, we can defined a default value. This value will be the value of this option until user customize it.
This default value is store directly in the option. So we can, at any moment we can go back to the default value.
The default value can be a :doc:`calculation`.
* - default_multi
- A second default value is available for multi option, `default_multi`. This value is used when we add new value without specified a value.
This `default_multi` must not be a list in multi purpose. For submulti, it has to be a list.
The default_multi value can be a :doc:`calculation`.
* - multi
- There are cases where it can be interesting to have a list of values rather than just one.
* - validators
- A list of :doc:`validator`.
* - properties
- A list of :doc:`property` (inside a frozenset().
* - warnings_only
- Only emit warnings if not type validation is invalid.
* - informations
- We can add default informations to this option.
Examples
==========
Let's try a simple option:
>>> from tiramisu import StrOption
>>> StrOption('welcome',
... 'Welcome message to the user login')
Add a default value:
>>> from tiramisu import StrOption
>>> StrOption('welcome',
... 'Welcome message to the user login',
... 'Hey guys, welcome here!')
Or a calculated default value:
>>> from tiramisu import StrOption, Calculation
>>> def get_value():
... return 'Hey guys, welcome here'
>>> StrOption('welcome',
... 'Welcome message to the user login',
... Calculation(get_value))
A multi option. In this case, the default value has to be a list:
>>> from tiramisu import StrOption
>>> StrOption('shopping_list',
... 'The shopping list',
... ['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'],
... multi=True)
The option could be a list of list, which is could submulti:
>>> from tiramisu import StrOption, submulti
>>> StrOption('shopping_list',
... 'The shopping list',
... [['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'],
... ['milk', 'eggs']],
... multi=submulti)
The default value can be a :doc:`calculation`. For a multi, the function have to return a list or have to be in a list:
>>> from tiramisu import StrOption, Calculation
>>> def get_values():
... return ['1 kilogram of carrots', 'leeks', '1 kilogram of potatos']
>>> StrOption('shopping_list',
... 'The shopping list',
... Calculation(get_values),
... multi=True)
or
>>> from tiramisu import StrOption, Calculation
>>> def get_a_value():
... return 'leeks'
>>> StrOption('shopping_list',
... 'The shopping list',
... ['1 kilogram of carrots', Calculation(get_a_value), '1 kilogram of potatos'],
... multi=True)
Add a default_multi:
>>> from tiramisu import StrOption, submulti
>>> StrOption('shopping_list',
... 'The shopping list',
... ['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'],
... default_multi='some vegetables',
... multi=True)
>>> StrOption('shopping_list',
... 'The shopping list',
... [['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'],
... ['milk', 'eggs']],
... default_multi=['some', 'vegetables'],
... multi=submulti)
Or calculated default_multi:
>>> from tiramisu import StrOption
>>> def get_a_value():
... return 'some vegetables'
>>> StrOption('shopping_list',
... 'The shopping list',
... ['1 kilogram of carrots', 'leeks', '1 kilogram of potatos'],
... default_multi=Calculation(get_a_value),
... multi=True)

View file

@ -0,0 +1,44 @@
==============================================
Generic container: :class:`OptionDescription`
==============================================
Option description
===================================
.. list-table::
:widths: 15 45
:header-rows: 1
* - Parameter
- Comments
* - name
- The `name` is important to retrieve this option.
* - doc
- The `description` allows the user to understand where this option will be used for.
* - children
- The list of children (Option) include inside.
.. note:: the option can be an :doc:`option` or an other option description
* - properties
- A list of :doc:`property` (inside a frozenset().
* - informations
- We can add default informations to this option description.
* - group_type
- Type for this group.
Examples
==============
>>> from tiramisu import StrOption, OptionDescription
>>> child1 = StrOption('first', 'First basic option')
>>> child2 = StrOption('second', 'Second basic option')
>>> child3 = StrOption('third', 'Third basic option')
>>> od1 = OptionDescription('od1', 'First option description', [child3])
>>> OptionDescription('basic',
... 'Basic options',
... [child1, child2, od1])

351
docs/options.rst Normal file
View file

@ -0,0 +1,351 @@
==================================
Default Options type
==================================
Basic options
==================================
Options
-----------
.. list-table::
:widths: 20 40 40
:header-rows: 1
* - Type
- Comments
- Extra parameters
* - StrOption
- Option that accept any textual data in Tiramisu.
-
* - IntOption
- Option that accept any integers number in Tiramisu.
-
- min_number
- max_number
* - FloatOption
- Option that accept any floating point number in Tiramisu.
-
* - BoolOption
- Boolean values are the two constant objects False and True.
-
Examples
------------
Textual option:
>>> from tiramisu import StrOption
>>> StrOption('str', 'str', 'value')
>>> try:
... StrOption('str', 'str', 1)
... except ValueError as err:
... print(err)
...
"1" is an invalid string for "str"
Integer option:
>>> from tiramisu import IntOption
>>> IntOption('int', 'int', 1)
>>> IntOption('int', 'int', 10, min_number=10, max_number=15)
>>> try:
... IntOption('int', 'int', 16, max_number=15)
... except ValueError as err:
... print(err)
...
"16" is an invalid integer for "int", value must be less than "15"
.. note:: If `warnings_only` parameter it set to True, it will only emit a warning.
Floating point number option:
>>> from tiramisu import FloatOption
>>> FloatOption('float', 'float', 10.1)
Boolean option:
>>> from tiramisu import BoolOption
>>> BoolOption('bool', 'bool', True)
>>> BoolOption('bool', 'bool', False)
Network options
==================================
.. list-table::
:widths: 20 40 40
:header-rows: 1
* - Type
- Comments
- Extra parameters
* - IPOption
- An Internet Protocol address (IP address) is a numerical label assigned to each device connected to a computer network that uses the Internet Protocol for communication.
This option only support version 4 of the Internet Protocol.
-
- private_only: restrict to only private IPv4 address
- allow_reserved: allow the IETF reserved address
- cidr: Classless Inter-Domain Routing (CIDR) is a method for allocating IP addresses and IP routing, such as 192.168.0.1/24
* - NetworkOption
- IP networks may be divided into subnetworks
-
- cidr: Classless Inter-Domain Routing (CIDR) is a method for allocating IP addresses and IP routing, such as 192.168.0.0/24
* - NetmaskOption
- For IPv4, a network may also be characterized by its subnet mask or netmask. This option allow you to enter a netmask.
-
* - BroadcastOption
- The last address within a network broadcast transmission to all hosts on the link. This option allow you to enter a broadcast.
-
* - PortOption
- A port is a network communication endpoint. It's a string object
-
- allow_range: allow is a list of port where we specified first port and last port number with the separator is `:`
- allow_zero: allow the port 0
- allow_wellknown: by default, the well-known ports (also known as system ports) those from 1 through 1023 are allowed, you can disabled it
- allow_registred: by default, the registered ports are those from 1024 through 49151 are allowed, you can disabled it
- allow_private: allow dynamic or private ports, which are those from 49152 through 65535, one common use for this range is for ephemeral ports
- allow_protocol: allow to define protocol in value, it should be something like tcp:80 or udp:53
* - MACOption
- MAC address for a network card.
Examples
-------------------------------------------
>>> from tiramisu import IPOption
>>> IPOption('ip', 'ip', '192.168.0.24')
>>> IPOption('ip', 'ip', '1.1.1.1')
>>> try:
... IPOption('ip', 'ip', '1.1.1.1', private_only=True)
... except ValueError as err:
... print(err)
...
"1.1.1.1" is an invalid IP for "ip", must be private IP
.. note:: If `warnings_only` parameter it set to True, it will only emit a warning.
>>> from tiramisu import IPOption
>>> try:
... IPOption('ip', 'ip', '255.255.255.255')
... except ValueError as err:
... print(err)
...
"255.255.255.255" is an invalid IP for "ip", mustn't be reserved IP
>>> IPOption('ip', 'ip', '255.255.255.255', allow_reserved=True)
.. note:: If `warnings_only` parameter it set to True, it will only emit a warning.
>>> from tiramisu import IPOption
>>> IPOption('ip', 'ip', '192.168.0.1/24', cidr=True)
>>> try:
... IPOption('ip', 'ip', '192.168.0.0/24', cidr=True)
... except ValueError as err:
... print(err)
...
"192.168.0.0/24" is an invalid IP for "ip", it's in fact a network address
>>> from tiramisu import NetworkOption
>>> NetworkOption('net', 'net', '192.168.0.0')
>>> NetworkOption('net', 'net', '192.168.0.0/24', cidr=True)
>>> NetmaskOption('mask', 'mask', '255.255.255.0')
>>> from tiramisu import BroadcastOption
>>> BroadcastOption('bcast', 'bcast', '192.168.0.254')
>>> from tiramisu import PortOption
>>> PortOption('port', 'port', '80')
>>> PortOption('port', 'port', '2000', allow_range=True)
>>> PortOption('port', 'port', '2000:3000', allow_range=True)
>>> from tiramisu import PortOption
>>> try:
... PortOption('port', 'port', '0')
... except ValueError as err:
... print(err)
...
"0" is an invalid port for "port", must be between 1 and 49151
>>> PortOption('port', 'port', '0', allow_zero=True)
.. note:: This option affect the minimal and maximal port number, if `warnings_only` parameter it set to True, it will only emit a warning.
>>> from tiramisu import PortOption
>>> PortOption('port', 'port', '80')
>>> try:
... PortOption('port', 'port', '80', allow_wellknown=False)
... except ValueError as err:
... print(err)
...
"80" is an invalid port for "port", must be between 1024 and 49151
.. note:: This option affect the minimal and maximal port number, if `warnings_only` parameter it set to True, it will only emit a warning.
>>> from tiramisu import PortOption
>>> PortOption('port', 'port', '1300')
>>> try:
... PortOption('port', 'port', '1300', allow_registred=False)
... except ValueError as err:
... print(err)
...
"1300" is an invalid port for "port", must be between 1 and 1023
.. note:: This option affect the minimal and maximal port number, if `warnings_only` parameter it set to True, it will only emit a warning.
>>> from tiramisu import PortOption
>>> try:
... PortOption('port', 'port', '64000')
... except ValueError as err:
... print(err)
...
"64000" is an invalid port for "port", must be between 1 and 49151
>>> PortOption('port', 'port', '64000', allow_private=True)
.. note:: This option affect the minimal and maximal port number, if `warnings_only` parameter it set to True, it will only emit a warning.
Internet options
==================================
.. list-table::
:widths: 20 40 40
:header-rows: 1
* - Type
- Comments
- Extra parameters
* - DomainnameOption
- Domain names are used in various networking contexts and for application-specific naming and addressing purposes.
-
- type: There is three type for a domain name:
- "domainname" (default): lowercase, number, "-" and "." characters are allowed, this must have at least one "."
- "hostname": lowercase, number and "-" characters are allowed, the maximum length is 63 characters
- "netbios": lowercase, number and "-" characters are allowed, the maximum length is 15 characters
- allow_ip: the option can contain a domain name or an IP, in this case, IP is validate has IPOption would do.
- allow_cidr_network: the option can contain a CIDR network
- allow_without_dot: a domain name with domainname's type must have a dot, if active, we can set a domainname or an hostname
- allow_startswith_dot: a domain name with domainname's type mustn't start by a dot, .example.net is not a valid domain, in some case it could be interesting to allow domain name starts by a dot (for ACL in Squid, no proxy option in Firefox, ...)
* - URLOption
- An Uniform Resource Locator is, in fact, a string starting with http:// or https://, a DomainnameOption, optionaly ':' and a PortOption, and finally filename
- See PortOption and DomainnameOption parameters
* - EmailOption
- Electronic mail (email or e-mail) is a method of exchanging messages ("mail") between people using electronic devices.
-
Examples
-----------------------------------------------
>>> from tiramisu import DomainnameOption
>>> DomainnameOption('domain', 'domain', 'foo.example.net')
>>> DomainnameOption('domain', 'domain', 'foo', type='hostname')
.. note:: If `warnings_only` parameter it set to True, it will raise if length is incorrect by only emit a warning character is not correct.
>>> from tiramisu import DomainnameOption
>>> DomainnameOption('domain', 'domain', 'foo.example.net', allow_ip=True)
>>> DomainnameOption('domain', 'domain', '192.168.0.1', allow_ip=True)
>>> DomainnameOption('domain', 'domain', 'foo.example.net', allow_cidr_network=True)
>>> DomainnameOption('domain', 'domain', '192.168.0.0/24', allow_cidr_network=True)
>>> DomainnameOption('domain', 'domain', 'foo.example.net', allow_without_dot=True)
>>> DomainnameOption('domain', 'domain', 'foo', allow_without_dot=True)
>>> DomainnameOption('domain', 'domain', 'example.net', allow_startswith_dot=True)
>>> DomainnameOption('domain', 'domain', '.example.net', allow_startswith_dot=True)
>>> from tiramisu import URLOption
>>> URLOption('url', 'url', 'http://foo.example.fr/index.php')
>>> URLOption('url', 'url', 'https://foo.example.fr:4200/index.php?login=foo&pass=bar')
>>> from tiramisu import EmailOption
>>> EmailOption('mail', 'mail', 'foo@example.net')
Unix options
===============
.. list-table::
:widths: 20 40
:header-rows: 1
* - Type
- Comments
- Extra parameters
* - UsernameOption
- An unix username option is a 32 characters maximum length with lowercase ASCII characters, number, '_' or '-'. The username have to start with lowercase ASCII characters or "_".
-
* - GroupnameOption
- Same conditions has username
-
* - PasswordOption
- Simple string with no other restriction:
-
- min_len: minimum length autorise for a password
- max_len: maximum length autorise for a passwword
- forbidden_char: list of forbidden characters for a password
* - FilenameOption
- For this option, only lowercase and uppercas ASCII character, "-", ".", "_", "~", and "/" are allowed.
-
- allow_relative: filename should starts with "/" (something like /etc/passwd), we can, with this option to allow relative name
- test_existence: file or directory should exists
- types
- file: it should be a file
- directory: it should be a directory
* - PermissionsOption
- Permissions for Unix file. It could be something like 644 or 1644.
-
>>> from tiramisu import UsernameOption
>>> UsernameOption('user', 'user', 'my_user')
>>> from tiramisu import GroupnameOption
>>> GroupnameOption('group', 'group', 'my_group')
>>> from tiramisu import PasswordOption
>>> PasswordOption('pass', 'pass', 'oP$¨1jiJie')
>>> from tiramisu import FilenameOption
>>> FilenameOption('file', 'file', '/etc/tiramisu/tiramisu.conf')
Date option
=============
Date option waits for a date with format YYYY-MM-DD:
>>> from tiramisu import DateOption
>>> DateOption('date', 'date', '2019-10-30')
Choice option: :class:`ChoiceOption`
======================================
Option that only accepts a list of possible choices.
For example, we just want allowed 1 or 'see later':
>>> from tiramisu import ChoiceOption
>>> ChoiceOption('choice', 'choice', (1, 'see later'), 1)
>>> ChoiceOption('choice', 'choice', (1, 'see later'), 'see later')
Any other value isn't allowed:
>>> try:
... ChoiceOption('choice', 'choice', (1, 'see later'), "i don't know")
... except ValueError as err:
... print(err)
...
"i don't know" is an invalid choice for "choice", only "1" and "see later" are allowed

123
docs/own_option.rst Normal file
View file

@ -0,0 +1,123 @@
======================================
Create it's own option
======================================
Generic regexp option: :class:`RegexpOption`
====================================================
Use `RegexpOption` to create custom option is very simple.
You just have to create an object that inherits from `RegexpOption` and that has the following class attributes:
- __slots__: with new data members (the values should always be `tuple()`)
- _type = with a name
- _regexp: with a compiled regexp
Here an example to an option that only accept string with on lowercase ASCII vowel characters:
.. literalinclude:: src/own_option.py
:lines: 3-10
:linenos:
Let's try our object:
>>> VowelOption('vowel', 'Vowel', 'aae')
<VowelOption object at 0x7feb2779c050>
>>> try:
... VowelOption('vowel', 'Vowel', 'oooups')
... except ValueError as err:
... print(err)
...
"oooups" is an invalid string with vowel for "Vowel"
Create your own option
=================================
An option always inherits from `Option` object. This object has the following class attributes:
- __slots__: with new data members (the values should always be `tuple()`)
- _type = with a name
Here an example to a lipogram option:
.. literalinclude:: src/own_option2.py
:lines: 3-12
:linenos:
First of all we want to add a custom parameter to ask the minimum length (`min_len`) of the value:
.. literalinclude:: src/own_option2.py
:lines: 13-17
:linenos:
We have a first validation method. In this method, we verify that the value is a string and that there is no "e" on it:
.. literalinclude:: src/own_option2.py
:lines: 19-26
:linenos:
Even if user set warnings_only attribute, this method will raise.
Finally we add a method to valid the value length. If `warnings_only` is set to True, a warning will be emit:
.. literalinclude:: src/own_option2.py
:lines: 28-40
:linenos:
Let's test it:
1. the character "e" is in the value:
>>> try:
... LipogramOption('lipo',
... 'Lipogram',
... 'I just want to add a quality string that has no bad characters')
... except ValueError as err:
... print(err)
...
"I just want to add a quality string that has no bad characters" is an invalid lipogram for "Lipogram", Perec wrote a book without any "e", you could not do it in a simple sentence?
2. the character "e" is in the value and warnings_only is set to True:
>>> try:
... LipogramOption('lipo',
... 'Lipogram',
... 'I just want to add a quality string that has no bad characters',
... warnings_only=True)
... except ValueError as err:
... print(err)
...
"I just want to add a quality string that has no bad characters" is an invalid lipogram for "Lipogram", Perec wrote a book without any "e", you could not do it in a simple sentence?
3. the value is too short
>>> try:
... LipogramOption('lipo',
... 'Lipogram',
... 'I just want to add a quality string that has no bad symbols')
... except ValueError as err:
... print(err)
...
"I just want to add a quality string that has no bad symbols" is an invalid lipogram for "Lipogram", you must have at least 100 characters in the sentence
4. the value is too short and warnings_only is set to True:
>>> warnings.simplefilter('always', ValueWarning)
>>> with warnings.catch_warnings(record=True) as warn:
... LipogramOption('lipo',
... 'Lipogram',
... 'I just want to add a quality string that has no bad symbols',
... warnings_only=True)
... if warn:
... print(warn[0].message)
...
attention, "I just want to add a quality string that has no bad symbols" could be an invalid lipogram for "Lipogram", it would be better to have at least 100 characters in the sentence
5. set minimum length to 50 characters, the value is valid:
>>> LipogramOption('lipo',
... 'Lipogram',
... 'I just want to add a quality string that has no bad symbols',
... min_len=50)

126
docs/property.rst Normal file
View file

@ -0,0 +1,126 @@
==================================
Properties
==================================
What are properties?
==================================
The properties are a central element of Tiramisu.
Properties change the behavior of an option or make it unavailable.
Read only and read write
=================================
Config can be in two defaut mode:
read_only
-----------------------
You can get all variables in a `Config` that not disabled.
Off course, as the Config is read only, you cannot set any value to any option.
Only value with :doc`calculation` can change value.
You cannot access to mandatory variable without values. Verify that all values is set before change mode.
read_write
-----------------------
You can get all options not disabled and not hidden. You can also set all variables not frozen.
Common properties
=================================
hidden
-----------------------
Option with this property can only get value in read only mode (or for a :doc:`calculation`).
This property is used for option that user cannot modifify it's value (for example if it's value is calculated).
disabled
-----------------------
We never can access to option with this property.
frozen
-----------------------
Options with this property cannot be modified.
Special option properties
=================================
mandatory
-----------------------
You should set value for option with this properties. In read only mode we cannot access to this option if no value is set.
empty or notempty
-----------------------
Only used for multi option that are not a follower.
Mandatory for a multi means that you cannot add None as a value. But value [] is allowed. This is not permit with `empty` property.
A multi option has automaticly `empty` property. If you don't want allow empty option, just add `notempty` property when you create the option.
unique or notunique
-----------------------
Only used for multi option that are not a follower.
Raise ValueError if a value is set twice or more in a multi Option.
A multi option has automaticly `unique` property. If you want allow duplication in option, just add `notunique` property when you create the option.
permissive
-----------------------
Option with 'permissive' cannot raise PropertiesOptionError for properties set in permissive.
Config with 'permissive', whole option in this config cannot raise PropertiesOptionError for properties set in permissive.
Special Config properties
=================================
cache
-----------------------
Enable cache settings and values.
expire
-----------------------
Enable settings and values in cache expire after `expiration_time` (by default 5 seconds).
everything_frozen
-----------------------
Whole option in config are frozen (even if option have not frozen property).
validator
-----------------------
Launch validator set by user in option (this property has no effect for option validation and second level validation).
warnings
-----------------------
Display warnings during validation.
demoting_error_warning
-----------------------
All value's errors are convert to warning (ValueErrorWarning).
Own properties
=================================
There are no specific instructions for creating a property. Just add a string as property create a new property.
.. #FIXME
.. FORBIDDEN_SET_PERMISSIVES = frozenset(['force_default_on_freeze',
.. 'force_metaconfig_on_freeze',
.. 'force_store_value'])

158
docs/quiz.rst Normal file
View file

@ -0,0 +1,158 @@
==================================
Bonus: Let's create a quiz!
==================================
Creating our quiz
==================================
Of course Tiramisu is great to handle options, but here's a little thing you can
do if you're just bored and don't know what to do.
So, let's create a quiz.
First, as always, let's import everything we need from Tiramisu and create our options:
.. literalinclude:: src/quiz.py
:lines: 1-6
:linenos:
We make a dictionary with all questions, proposals and answer:
.. literalinclude:: src/quiz.py
:lines: 20-35
:linenos:
Now build our Config.
We have to create question option.
Just after, we're going to define the correct answer in a second option.
The answer is frozen so that when it is set, it cannot change.
And finally a last option that will verify if the answer is correct.
.. literalinclude:: src/quiz.py
:lines: 38-59
:linenos:
The `verif` option will us a function that will verify if the answer given by the user
(which will become the value of `question`) is the same as the correct answer (which is the value
of `answer`). Here is this function (of course you have to declare at the begining of your code,
before your options) :
.. literalinclude:: src/quiz.py
:lines: 12-13
:linenos:
Pretty simple.
At least we're done with our questions. Let's just create one last option.
This option calculate the result of the students' answers:
.. literalinclude:: src/quiz.py
:lines: 16-17, 62-65
:linenos:
Now we just have to create our OptionDescription and Config (well it's a MetaConfig
here, but we'll see this later)...
.. literalinclude:: src/quiz.py
:lines: 66, 9, 67
:linenos:
... and add some loops to run our quiz!
.. literalinclude:: src/quiz.py
:lines: 70-104
:linenos:
Display results for teacher:
.. literalinclude:: src/quiz.py
:lines: 107-117
:linenos:
Now let's play !
.. literalinclude:: src/quiz.py
:lines: 120-132
Download the :download:`full code <src/quiz.py>`
Hey, that was easy ! Almost like I already knew the answers... Oh wait...
Get players results
==================================
Now that you have your quiz, you can play with friends! And what's better than playing
with friends? Crushing them by comparing your scores of course!
You may have noticed that the previous code had some storage instructions. Now we can
create a score board that will give each player's latest score with their errors !
We created a meta config that will be used as a base for all configs we will create :
each time a new player name will be entered, a new config will be created, with the new player's
name as it's session id. This way, we can see every player's result !
So, earlier, we created a MetaConfig, and set the storage on sqlite3, so our data will
not be deleted after the quiz stops to run.
Let's run the script:
| Who are you? (a student | a teacher): a student
| Enter a name: my name
| Question 1: what does the cat say?
| woof | meow
| Your answer: meow
| Correct answer!
|
| Question 2: what do you get by mixing blue and yellow?
| green | red | purple
| Your answer: green
| Correct answer!
|
| Question 3: where is Bryan?
| at school | in his bedroom | in the kitchen
| Your answer: at school
| Wrong answer... the correct answer was: in the kitchen
|
| Question 4: which one has 4 legs and 2 wings?
| a wyvern | a dragon | a wyrm | a drake
| Your answer: a dragon
| Correct answer!
|
| Question 5: why life?
| because | I don't know | good question
| Your answer: good question
| Correct answer!
|
| Correct answers: 4 out of 5
When the quiz runs, we will create a new Config in our MetaConfig:
.. literalinclude:: src/quiz.py
:lines: 70-75
:linenos:
:emphasize-lines: 3
All results are store in this config (so in the database). So we need to reload those previous config:
.. literalinclude:: src/quiz.py
:lines: 120-124
:linenos:
:emphasize-lines: 4
Later, a teacher ca display all those score:
| Who are you? (a student | a teacher): a teacher
| ==================== my name ==========================
| Question 1: correct answer
| Question 2: correct answer
| Question 3: wrong answer: at school
| Question 4: correct answer
| Question 5: correct answer
| my name's score: 4 out of 5
You've got everything now, so it's your turn to create your own questions and play with
your friends !

81
docs/requirements.txt Normal file
View file

@ -0,0 +1,81 @@
alabaster==0.7.13
asttokens==2.4.1
attrs==23.1.0
Babel==2.13.1
certifi==2023.7.22
charset-normalizer==3.3.2
click==8.1.7
colorama==0.4.6
comm==0.2.0
debugpy==1.8.0
decorator==5.1.1
docutils==0.18.1
exceptiongroup==1.1.3
executing==2.0.1
fastjsonschema==2.18.1
greenlet==3.0.1
idna==3.4
imagesize==1.4.1
importlib-metadata==6.8.0
ipykernel==6.26.0
ipython==8.17.2
jedi==0.19.1
Jinja2==3.1.2
jsonschema==4.19.2
jsonschema-specifications==2023.7.1
jupyter-cache==1.0.0
jupyter_client==8.6.0
jupyter_core==5.5.0
livereload==2.6.3
markdown-it-py==3.0.0
MarkupSafe==2.1.3
matplotlib-inline==0.1.6
mdit-py-plugins==0.4.0
mdurl==0.1.2
myst-nb==1.0.0
myst-parser==2.0.0
nbclient==0.9.0
nbformat==5.9.2
nest-asyncio==1.5.8
packaging==23.2
parso==0.8.3
pexpect==4.8.0
platformdirs==4.0.0
prompt-toolkit==3.0.40
psutil==5.9.6
ptyprocess==0.7.0
pure-eval==0.2.2
Pygments==2.16.1
python-dateutil==2.8.2
PyYAML==6.0.1
pyzmq==25.1.1
referencing==0.30.2
requests==2.31.0
rpds-py==0.12.0
six==1.16.0
snowballstemmer==2.2.0
Sphinx==7.2.6
sphinx-autobuild==2021.3.14
sphinx-copybutton==0.5.2
sphinx-lesson==0.8.15
sphinx-minipres==0.2.1
sphinx-rtd-theme==1.3.0
sphinx-rtd-theme-ext-color-contrast==0.3.1
sphinx-tabs==3.4.4
sphinx-togglebutton==0.3.2
sphinxcontrib-applehelp==1.0.7
sphinxcontrib-devhelp==1.0.5
sphinxcontrib-htmlhelp==2.0.4
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.6
sphinxcontrib-serializinghtml==1.1.9
SQLAlchemy==2.0.23
stack-data==0.6.3
tabulate==0.9.0
tornado==6.3.3
traitlets==5.13.0
typing_extensions==4.8.0
urllib3==2.0.7
wcwidth==0.2.9
zipp==3.17.0

18
docs/src/Makefile Normal file
View file

@ -0,0 +1,18 @@
SRC=$(wildcard *.py)
HTMLFRAGMENT=$(addsuffix .html, $(basename $(SRC)))
.SUFFIXES:
.PHONY: all clean
all: html
# make -C ./build all
html: $(HTMLFRAGMENT)
%.html: %.py
pygmentize -f html -O full,bg=white,style=bw, -o ../en/_build/html/_modules/$@ $<
clean:
# make -C ./build clean

View file

@ -0,0 +1,28 @@
from os.path import exists
from tiramisu import FilenameOption, BoolOption, OptionDescription, Leadership, \
Config, Calculation, Params, ParamOption, ParamValue, calc_value
from tiramisu.error import PropertiesOptionError, ConfigError
def inverse(exists_):
return not exists_
filename = FilenameOption('filename',
'Filename',
multi=True,
properties=('mandatory',))
exists_ = BoolOption('exists',
'This file exists',
Calculation(exists, Params(ParamOption(filename))),
multi=True,
properties=('frozen', 'force_default_on_freeze', 'advanced'))
create = BoolOption('create',
'Create automaticly the file',
multi=True,
default_multi=Calculation(inverse, Params(ParamOption(exists_))))
new = Leadership('new',
'Add new file',
[filename, exists_, create])
root = OptionDescription('root', 'root', [new])
config = Config(root)

View file

@ -0,0 +1,20 @@
from os.path import exists
from tiramisu import FilenameOption, BoolOption, OptionDescription, Leadership, \
Config, Calculation, Params, ParamOption
from tiramisu.error import PropertiesOptionError
filename = FilenameOption('filename',
'Filename',
multi=True,
properties=('mandatory',))
exists_ = BoolOption('exists',
'This file exists',
Calculation(exists, Params(ParamOption(filename))),
multi=True,
properties=('frozen', 'force_default_on_freeze', 'advanced'))
new = Leadership('new',
'Add new file',
[filename, exists_])
root = OptionDescription('root', 'root', [new])
config = Config(root)

View file

@ -0,0 +1,147 @@
from stat import S_IMODE, S_ISDIR, S_ISSOCK
from os import lstat, getuid, getgid
from os.path import exists
from pwd import getpwuid
from grp import getgrgid
from tiramisu import FilenameOption, UsernameOption, GroupnameOption, IntOption, BoolOption, ChoiceOption, \
OptionDescription, Leadership, Config, Calculation, Params, ParamSelfOption, ParamOption, ParamValue, \
calc_value
from tiramisu.error import LeadershipError, PropertiesOptionError
def get_username(filename, exists, create=False):
if exists:
uid = lstat(filename).st_uid
elif create:
# the current uid
uid = getuid()
else:
return
return getpwuid(uid).pw_name
def get_grpname(filename, exists, create=False):
if exists:
gid = lstat(filename).st_gid
elif create:
# the current gid
gid = getgid()
else:
return
return getgrgid(gid).gr_name
def calc_type(filename, is_exists):
if is_exists:
mode = lstat(filename).st_mode
if S_ISSOCK(mode):
return 'socket'
elif S_ISDIR(mode):
return 'directory'
return 'file'
def calc_mode(filename, is_exists, type):
if is_exists:
return int(oct(S_IMODE(lstat(filename).st_mode))[2:])
if type == 'file':
return 644
elif type == 'directory':
return 755
elif type == 'socket':
return 444
filename = FilenameOption('filename',
'Filename',
multi=True,
properties=('mandatory',))
exists_ = BoolOption('exists',
'This file exists',
Calculation(exists, Params(ParamOption(filename))),
multi=True,
properties=('mandatory', 'frozen', 'force_default_on_freeze', 'advanced'))
create = BoolOption('create',
'Create automaticly the file',
multi=True,
default_multi=True,
properties=(Calculation(calc_value,
Params(ParamValue('disabled'),
kwargs={'condition': ParamOption(exists_),
'expected': ParamValue(True)})),))
type_ = ChoiceOption('type',
'The file type',
('file', 'directory', 'socket'),
Calculation(calc_type, Params((ParamOption(filename),
ParamOption(exists_)))),
multi=True,
properties=('force_default_on_freeze', 'mandatory',
Calculation(calc_value,
Params(ParamValue('hidden'),
kwargs={'condition': ParamOption(exists_),
'expected': ParamValue(True)})),
Calculation(calc_value,
Params(ParamValue('frozen'),
kwargs={'condition': ParamOption(exists_),
'expected': ParamValue(True)}))))
username = UsernameOption('user',
'User',
default_multi=Calculation(get_username, Params((ParamOption(filename),
ParamOption(exists_),
ParamOption(create, notraisepropertyerror=True)))),
multi=True,
properties=('force_store_value',
Calculation(calc_value,
Params(ParamValue('mandatory'),
kwargs={'condition': ParamOption(create, notraisepropertyerror=True),
'expected': ParamValue(True),
'no_condition_is_invalid': ParamValue(True)})),))
grpname = GroupnameOption('group',
'Group',
default_multi=Calculation(get_grpname, Params((ParamOption(filename),
ParamOption(exists_),
ParamOption(create, notraisepropertyerror=True)))),
multi=True,
properties=('force_store_value',
Calculation(calc_value,
Params(ParamValue('mandatory'),
kwargs={'condition': ParamOption(create, notraisepropertyerror=True),
'expected': ParamValue(True),
'no_condition_is_invalid': ParamValue(True)})),))
mode = IntOption('mode',
'Mode',
default_multi=Calculation(calc_mode, Params((ParamOption(filename), ParamOption(exists_), ParamOption(type_)))),
multi=True,
properties=('mandatory', 'advanced', 'force_store_value'))
new = Leadership('new',
'Add new file',
[filename, exists_, create, type_, username, grpname, mode])
root = OptionDescription('root', 'root', [new])
config = Config(root)
#config.option('new.create', 1).value.set(False)
#config.option('new.type', 1).value.set('file')
#config.option('new.type', 2).value.set('file')
#print(config.value.dict())
#config.option('new.type', 2).value.set('directory')
#print(config.value.dict())
#print(config.unrestraint.option('new.mode', 0).owner.isdefault())
#print(config.unrestraint.option('new.mode', 1).owner.isdefault())
#print(config.unrestraint.option('new.mode', 2).owner.isdefault())
#config.property.read_only()
#print(config.option('new.mode', 0).owner.isdefault())
#print(config.option('new.mode', 1).owner.isdefault())
#print(config.option('new.mode', 2).owner.isdefault())
#print(config.value.dict())
#config.property.read_write()
#config.option('new.type', 2).value.set('file')
#print(config.value.dict())
#config.option('new.mode', 2).value.reset()
#print(config.value.dict())

31
docs/src/api_value.py Normal file
View file

@ -0,0 +1,31 @@
from shutil import disk_usage
from os.path import isdir
from tiramisu import FilenameOption, FloatOption, OptionDescription, Config, \
Calculation, Params, ParamValue, ParamOption, ParamSelfOption
def valid_is_dir(path):
# verify if path is a directory
if not isdir(path):
raise ValueError('this directory does not exist')
def calc_disk_usage(path, size='bytes'):
# do not calc if path is None
if path is None:
return None
if size == 'bytes':
div = 1
else:
# bytes to gigabytes
div = 1024 * 1024 * 1024
return disk_usage(path).free / div
filename = FilenameOption('path', 'Path', validators=[Calculation(valid_is_dir,
Params(ParamSelfOption()))])
usage = FloatOption('usage', 'Disk usage', Calculation(calc_disk_usage,
Params(ParamOption(filename))))
disk = OptionDescription('disk', 'Verify disk usage', [filename, usage])
root = OptionDescription('root', 'root', [disk])
config = Config(root)
config.property.read_write()

View file

@ -0,0 +1,33 @@
from shutil import disk_usage
from os.path import isdir
from tiramisu import FilenameOption, FloatOption, ChoiceOption, OptionDescription, Config, \
Calculation, Params, ParamValue, ParamOption, ParamSelfOption
def valid_is_dir(path):
# verify if path is a directory
if not isdir(path):
raise ValueError('this directory does not exist')
def calc_disk_usage(path, size='bytes'):
# do not calc if path is None
if path is None:
return None
if size == 'bytes':
div = 1
else:
# bytes to gigabytes
div = 1024 * 1024 * 1024
return disk_usage(path).free / div
filename = FilenameOption('path', 'Path', validators=[Calculation(valid_is_dir,
Params(ParamSelfOption()))])
size_type = ChoiceOption('size_type', 'Size type', ('bytes', 'giga bytes'), 'bytes')
usage = FloatOption('usage', 'Disk usage', Calculation(calc_disk_usage,
Params((ParamOption(filename),
ParamOption(size_type)))))
disk = OptionDescription('disk', 'Verify disk usage', [filename, size_type, usage])
root = OptionDescription('root', 'root', [disk])
config = Config(root)
config.property.read_write()

View file

@ -0,0 +1,34 @@
from shutil import disk_usage
from os.path import isdir
from tiramisu import FilenameOption, FloatOption, ChoiceOption, OptionDescription, Leadership, \
Config, \
Calculation, Params, ParamValue, ParamOption, ParamSelfOption
def valid_is_dir(path):
# verify if path is a directory
if not isdir(path):
raise ValueError('this directory does not exist')
def calc_disk_usage(path, size):
if size == 'bytes':
div = 1
else:
# bytes to gigabytes
div = 1024 * 1024 * 1024
return disk_usage(path).free / div
filename = FilenameOption('path', 'Path', validators=[Calculation(valid_is_dir,
Params(ParamSelfOption(whole=False)))],
multi=True)
size_type = ChoiceOption('size_type', 'Size type', ('bytes', 'giga bytes'),
default_multi='bytes', multi=True)
usage = FloatOption('usage', 'Disk usage', Calculation(calc_disk_usage,
Params((ParamOption(filename),
ParamOption(size_type)))),
multi=True)
disk = Leadership('disk', 'Verify disk usage', [filename, size_type, usage])
root = OptionDescription('root', 'root', [disk])
config = Config(root)
config.property.read_write()

View file

@ -0,0 +1,34 @@
from shutil import disk_usage
from os.path import isdir
from tiramisu import FilenameOption, FloatOption, ChoiceOption, OptionDescription, Config, \
Calculation, Params, ParamValue, ParamOption, ParamSelfOption
def valid_is_dir(path):
# verify if path is a directory
if not isdir(path):
raise ValueError('this directory does not exist')
def calc_disk_usage(paths, size='bytes'):
if size == 'bytes':
div = 1
else:
# bytes to gigabytes
div = 1024 * 1024 * 1024
ret = []
for path in paths:
ret.append(disk_usage(path).free / div)
return ret
filename = FilenameOption('path', 'Path', validators=[Calculation(valid_is_dir,
Params(ParamSelfOption(whole=False)))],
multi=True)
size_type = ChoiceOption('size_type', 'Size type', ('bytes', 'giga bytes'), 'bytes')
usage = FloatOption('usage', 'Disk usage', Calculation(calc_disk_usage,
Params((ParamOption(filename),
ParamOption(size_type)))),
multi=True)
disk = OptionDescription('disk', 'Verify disk usage', [filename, size_type, usage])
root = OptionDescription('root', 'root', [disk])
config = Config(root)
config.property.read_write()

232
docs/src/application.py Normal file
View file

@ -0,0 +1,232 @@
from tiramisu import BoolOption, ChoiceOption, DomainnameOption, PortOption, URLOption, \
OptionDescription, Calculation, Params, ParamOption, ParamValue, \
Config, calc_value, calc_value_property_help
def protocols_settings(use: bool, value):
if use is True:
return value
# this option's value will determine which of the others options are frozen and which are not thanks
proxy_mode = ChoiceOption('proxy_mode',
'Proxy\'s config mode',
('No proxy',
'Auto-detect proxy settings for this network',
'Use system proxy settings',
'Manual proxy configuration',
'Automatic proxy configuration URL'),
default = 'No proxy',
properties=('mandatory',))
http_address = DomainnameOption('http_address',
'Address',
allow_ip=True,
properties=('mandatory',))
http_port = PortOption('http_port',
'Port',
default='8080',
properties=('mandatory',))
http_proxy = OptionDescription('http_proxy',
'HTTP Proxy',
[http_address, http_port])
use_for_all_protocols = BoolOption('use_for_all_protocols',
'Use HTTP IP and Port for all protocols',
default=True)
# if this option is valued with 'True', set all the others IP and port values to the same as HTTP IP and port.
ssl_address = DomainnameOption('ssl_address',
'Address',
Calculation(protocols_settings,
Params((ParamOption(use_for_all_protocols), ParamOption(http_address)))),
allow_ip=True,
properties=('mandatory', 'force_default_on_freeze',
Calculation(calc_value,
Params(ParamValue('frozen'),
kwargs={'condition': ParamOption(use_for_all_protocols, todict=True),
'expected': ParamValue(True)}),
calc_value_property_help)))
ssl_port = PortOption('ssl_port',
'Port',
Calculation(protocols_settings,
Params((ParamOption(use_for_all_protocols), ParamOption(http_port)))),
properties=('mandatory', 'force_default_on_freeze',
Calculation(calc_value,
Params(ParamValue('frozen'),
kwargs={'condition': ParamOption(use_for_all_protocols, todict=True),
'expected': ParamValue(True)}),
calc_value_property_help)))
ssl_proxy = OptionDescription('ssl_proxy',
'SSL Proxy',
[ssl_address, ssl_port],
properties=(Calculation(calc_value,
Params(ParamValue('hidden'),
kwargs={'condition': ParamOption(use_for_all_protocols, todict=True),
'expected': ParamValue(True)}),
calc_value_property_help),))
ftp_address = DomainnameOption('ftp_address',
'Address',
Calculation(protocols_settings,
Params((ParamOption(use_for_all_protocols), ParamOption(http_address)))),
allow_ip=True,
properties=('mandatory', 'force_default_on_freeze',
Calculation(calc_value,
Params(ParamValue('frozen'),
kwargs={'condition': ParamOption(use_for_all_protocols, todict=True),
'expected': ParamValue(True)}),
calc_value_property_help)))
ftp_port = PortOption('ftp_port',
'Port',
Calculation(protocols_settings,
Params((ParamOption(use_for_all_protocols), ParamOption(http_port)))),
properties=('force_default_on_freeze',
Calculation(calc_value,
Params(ParamValue('frozen'),
kwargs={'condition': ParamOption(use_for_all_protocols, todict=True),
'expected': ParamValue(True)}),
calc_value_property_help)))
ftp_proxy = OptionDescription('ftp_proxy',
'FTP Proxy',
[ftp_address, ftp_port],
properties=(Calculation(calc_value,
Params(ParamValue('hidden'),
kwargs={'condition': ParamOption(use_for_all_protocols, todict=True),
'expected': ParamValue(True)}),
calc_value_property_help),))
socks_address = DomainnameOption('socks_address',
'Address',
Calculation(protocols_settings,
Params((ParamOption(use_for_all_protocols), ParamOption(http_address)))),
allow_ip=True,
properties=('mandatory', 'force_default_on_freeze',
Calculation(calc_value,
Params(ParamValue('frozen'),
kwargs={'condition': ParamOption(use_for_all_protocols, todict=True),
'expected': ParamValue(True)}),
calc_value_property_help)))
socks_port = PortOption('socks_port',
'Port',
Calculation(protocols_settings,
Params((ParamOption(use_for_all_protocols), ParamOption(http_port)))),
properties=('mandatory', 'force_default_on_freeze',
Calculation(calc_value,
Params(ParamValue('frozen'),
kwargs={'condition': ParamOption(use_for_all_protocols, todict=True),
'expected': ParamValue(True)}),
calc_value_property_help)))
socks_version = ChoiceOption('socks_version',
'SOCKS host version used by proxy',
('v4', 'v5'),
default='v5',
properties=('force_default_on_freeze',
Calculation(calc_value,
Params(ParamValue('frozen'),
kwargs={'condition': ParamOption(use_for_all_protocols, todict=True),
'expected': ParamValue(True)}),
calc_value_property_help)))
socks_proxy = OptionDescription('socks_proxy',
'Socks host proxy',
[socks_address, socks_port, socks_version],
properties=(Calculation(calc_value,
Params(ParamValue('hidden'),
kwargs={'condition': ParamOption(use_for_all_protocols, todict=True),
'expected': ParamValue(True)}),
calc_value_property_help),))
protocols = OptionDescription('protocols',
'Protocols parameters',
[http_proxy,
use_for_all_protocols,
ssl_proxy,
ftp_proxy,
socks_proxy],
properties=(Calculation(calc_value,
Params(ParamValue('disabled'),
kwargs={'condition': ParamOption(proxy_mode, todict=True),
'expected': ParamValue('Manual proxy configuration'),
'reverse_condition': ParamValue(True)}),
calc_value_property_help),))
auto_config_url = URLOption('auto_config_url',
'Proxy\'s auto config URL',
allow_ip=True,
properties=('mandatory',
Calculation(calc_value,
Params(ParamValue('disabled'),
kwargs={'condition': ParamOption(proxy_mode, todict=True),
'expected': ParamValue('Automatic proxy configuration URL'),
'reverse_condition': ParamValue(True)}),
calc_value_property_help),))
no_proxy = DomainnameOption('no_proxy',
'Address for which proxy will be desactivated',
multi=True,
allow_ip=True,
allow_cidr_network=True,
allow_without_dot=True,
allow_startswith_dot=True,
properties=(Calculation(calc_value,
Params(ParamValue('disabled'),
kwargs={'condition': ParamOption(proxy_mode, todict=True),
'expected': ParamValue('No proxy')}),
calc_value_property_help),))
prompt_authentication = BoolOption('prompt_authentication',
'Prompt for authentication if password is saved',
default=False,
properties=(Calculation(calc_value,
Params(ParamValue('disabled'),
kwargs={'condition': ParamOption(proxy_mode, todict=True),
'expected': ParamValue('No proxy')}),
calc_value_property_help),))
proxy_dns_socks5 = BoolOption('proxy_dns_socks5',
'Use Proxy DNS when using SOCKS v5',
default=False,
properties=(Calculation(calc_value,
Params(ParamValue('disabled'),
kwargs={'condition_1': ParamOption(socks_version,
raisepropertyerror=True),
'expected_1': ParamValue('v4'),
'condition_2': ParamOption(proxy_mode, todict=True),
'expected_2': ParamValue('No proxy'),
'condition_operator': ParamValue('OR')}),
calc_value_property_help),))
enable_dns_over_https = BoolOption('enable_dns_over_https',
'Enable DNS over HTTPS',
default=False)
used_dns = ChoiceOption('used_dns',
'Used DNS',
('default', 'custom'),
properties=(Calculation(calc_value,
Params(ParamValue('disabled'),
kwargs={'condition': ParamOption(enable_dns_over_https, todict=True),
'expected': ParamValue(False)}),
calc_value_property_help),))
custom_dns_url = URLOption('custom_dns_url',
'Custom DNS URL',
properties=(Calculation(calc_value,
Params(ParamValue('disabled'),
kwargs={'condition': ParamOption(used_dns, todict=True,
raisepropertyerror=True),
'expected': ParamValue('default')}),
calc_value_property_help),))
dns_over_https = OptionDescription('dns_over_https',
'DNS over HTTPS',
[enable_dns_over_https, used_dns, custom_dns_url])
rootod = OptionDescription('proxy',
'Proxy parameters',
[proxy_mode,
protocols,
no_proxy,
auto_config_url,
prompt_authentication,
proxy_dns_socks5, dns_over_https])
proxy_config = Config(rootod)
proxy_config.property.read_write()

73
docs/src/calculation.py Normal file
View file

@ -0,0 +1,73 @@
from tiramisu import Config, OptionDescription, Leadership, IntOption, Params, ParamOption, ParamValue, ParamContext, ParamIndex
def a_function():
pass
Calculation(a_function)
def a_function_with_parameters(value1, value2):
return value1 + ' ' + value2
Calculation(a_function_with_parameters, Params(ParamValue('my value 1'), kwargs={value2: ParamValue('my value 2')}))
def a_function_with_parameters(value1, value2):
return value1 + ' ' + value2
Calculation(a_function_with_parameters, Params((ParamValue('my value 1'), ParamValue('my value 2'))))
def a_function_with_option(option1):
return option1
option1 = IntOption('option1', 'first option', 1)
Calculation(a_function_with_option, Params(ParamOption(option1)))
def a_function_with_option(option1):
return option1
option1 = IntOption('option1', 'first option', 1, properties=('disabled',))
Calculation(a_function_with_option, Params(ParamOption(option1)))
def a_function_with_option(option1):
return option1
Calculation(a_function_with_option, Params(ParamOption(option1, raisepropertyerror=True)))
def a_function_with_option(option1=None):
return option1
Calculation(a_function_with_option, Params(ParamOption(option1, notraisepropertyerror=True)))
def a_function_with_dict_option(option1):
return "the option {} has value {}".format(option1['name'], option1['value'])
Calculation(a_function_with_option, Params(ParamOption(todict=True)))
def a_function_with_context(context):
pass
Calculation(a_function_with_context, Params(ParamContext()))
def a_function_multi(option1):
return option1
option1 = IntOption('option1', 'option1', [1], multi=True)
Calculation(a_function, Params(ParamOption(option1)))
def a_function_leader(option):
return option
leader = IntOption('leader', 'leader', [1], multi=True)
follower1 = IntOption('follower1', 'follower1', default_multi=2, multi=True)
follower2 = IntOption('follower2', 'follower2', default_multi=3, multi=True)
leadership = Leadership('leadership', 'leadership', [leader, follower1, follower2])
Calculation(a_function_leader, Params(ParamOption(leader)))
def a_function_follower(follower):
return follower
leader = IntOption('leader', 'leader', [1], multi=True)
follower1 = IntOption('follower1', 'follower1', default_multi=2, multi=True)
follower2 = IntOption('follower2', 'follower2', default_multi=3, multi=True)
leadership = Leadership('leadership', 'leadership', [leader, follower1, follower2])
Calculation(a_function_follower, Params(ParamOption(follower1)))
def a_function_index(index):
return index
leader = IntOption('leader', 'leader', [1], multi=True)
follower1 = IntOption('follower1', 'follower1', default_multi=2, multi=True)
follower2 = IntOption('follower2', 'follower2', default_multi=3, multi=True)
leadership = Leadership('leadership', 'leadership', [leader, follower1, follower2])
Calculation(a_function_index, Params(ParamIndex()))

6
docs/src/find.py Normal file
View file

@ -0,0 +1,6 @@
"this is only to make sure that the tiramisu library loads properly"
cfg = Config(rootod)

View file

@ -0,0 +1,19 @@
"getting started with the tiramisu library (it loads and prints properly)"
from tiramisu import Config
from tiramisu import OptionDescription, BoolOption
# let's create a group of options
descr = OptionDescription("optgroup", "", [
# ... with only one option inside
BoolOption("bool", "", default=False)
])
cfg = Config(descr)
# the global help about the config
cfg.help()
# help about an option
cfg.option("bool").help()
# the config's __repr__
print(cfg)

10
docs/src/own_option.py Normal file
View file

@ -0,0 +1,10 @@
#!/usr/bin/env python3
import re
from tiramisu import RegexpOption
class VowelOption(RegexpOption):
__slots__ = tuple()
_type = 'vowel'
_regexp = re.compile(r"^[aeiouy]*$")

40
docs/src/own_option2.py Normal file
View file

@ -0,0 +1,40 @@
#!/usr/bin/env python3
from tiramisu import Option
class LipogramOption(Option):
__slots__ = tuple()
_type = 'lipogram'
def __init__(self,
*args,
min_len=100,
**kwargs):
# store extra parameters
extra = {'_min_len': min_len}
super().__init__(*args,
extra=extra,
**kwargs)
def validate(self,
value):
# first, valid that the value is a string
if not isinstance(value, str):
raise ValueError('invalid string')
# and verify that there is any 'e' in the sentense
if 'e' in value:
raise ValueError('Perec wrote a book without any "e", you could not do it in a simple sentence?')
def second_level_validation(self,
value,
warnings_only):
# retrive parameter in extra
min_len = self.impl_get_extra('_min_len')
# verify the sentense length
if len(value) < min_len:
# raise message, in this case, warning and error message are different
if warnings_only:
msg = f'it would be better to have at least {min_len} characters in the sentence'
else:
msg = f'you must have at least {min_len} characters in the sentence'
raise ValueError(msg)

20
docs/src/property.py Normal file
View file

@ -0,0 +1,20 @@
from tiramisu import Config
from tiramisu import StrOption, OptionDescription
# let's declare some options
var1 = StrOption('var1', 'first option')
# an option with a default value
var2 = StrOption('var2', 'second option', u'value')
# let's create a group of options
od1 = OptionDescription('od1', 'first OD', [var1, var2])
# let's create another group of options
rootod = OptionDescription('rootod', '', [od1])
# let's create the config
cfg = Config(rootod)
# the api is read only
cfg.property.read_only()
# the read_write api is available
cfg.property.read_write()

51
docs/src/proxy.py Normal file
View file

@ -0,0 +1,51 @@
from tiramisu import IPOption, PortOption, BoolOption, ChoiceOption, DomainnameOption, \
URLOption, NetworkOption, NetmaskOption, \
SymLinkOption, OptionDescription, Leadership, Config
proxy_mode = ChoiceOption('proxy_mode', 'Proxy\'s config mode', ('No proxy',
'Manual proxy configuration',
'Automatic proxy configuration URL'),
properties=('positional', 'mandatory'))
http_ip_address = IPOption('http_ip_address', 'Proxy\'s HTTP IP', properties=('mandatory',))
http_ip_short = SymLinkOption('i', http_ip_address)
http_port = PortOption('http_port', 'Proxy\'s HTTP Port', default='8080', properties=('mandatory',))
http_port_short = SymLinkOption('p', http_port)
manual_proxy = OptionDescription('manual_proxy', 'Manual proxy settings', [http_ip_address, http_ip_short, http_port, http_port_short],
requires=[{'option': proxy_mode, 'expected': 'Manual proxy configuration', 'action':'disabled', 'inverse':True}])
auto_config_url = URLOption('auto_config_url','Proxy\'s auto config URL', properties=('mandatory',))
auto_config_url_short = SymLinkOption('i', auto_config_url)
automatic_proxy = OptionDescription('automatic_proxy', 'Automatic proxy setting',
[auto_config_url, auto_config_url_short],
requires=[{'option': proxy_mode, 'expected': 'Automatic proxy configuration URL', 'action':'disabled', 'inverse': True}])
configuration = OptionDescription('configuration', None,
[manual_proxy, automatic_proxy])
no_proxy_domain = DomainnameOption('no_proxy_domain', 'Domain names for which proxy will be desactivated', multi=True)
no_proxy_network = NetworkOption('no_proxy_network', 'Network addresses', multi=True)
no_proxy_network_short = SymLinkOption('n', no_proxy_network)
no_proxy_netmask = NetmaskOption('no_proxy_netmask', 'Netmask addresses', multi=True, properties=('mandatory',))
no_proxy_network_leadership = Leadership('no_proxy_network', 'Network for which proxy will be desactivated', [no_proxy_network, no_proxy_netmask])
no_proxy = OptionDescription('no_proxy', 'Disabled proxy',
[no_proxy_domain, no_proxy_network_leadership],
requires=[{'option': proxy_mode, 'expected': 'No proxy', 'action':'disabled'}, {'option': proxy_mode, 'expected': None, 'action':'disabled'}])
dns_over_https = BoolOption('dns_over_https', 'Enable DNS over HTTPS', default=False)
root = OptionDescription('proxy', 'Proxy parameters',
[proxy_mode, configuration, no_proxy, dns_over_https])
def display_name(option, dyn_name):
return "--" + option.impl_getpath()
proxy_config = Config(root, display_name=display_name)
proxy_config.property.read_write()
from tiramisu_cmdline_parser import TiramisuCmdlineParser
parser = TiramisuCmdlineParser(proxy_config)
parser.parse_args()
from pprint import pprint
pprint(proxy_config.value.dict())

View file

@ -0,0 +1,54 @@
from tiramisu import IPOption, PortOption, BoolOption, ChoiceOption, DomainnameOption, \
URLOption, NetworkOption, NetmaskOption, \
SymLinkOption, OptionDescription, Leadership, Config
proxy_mode = ChoiceOption('proxy_mode', 'Proxy\'s config mode', ('No proxy',
'Manual proxy configuration',
'Automatic proxy configuration URL'),
properties=('positional', 'mandatory'))
http_ip_address = IPOption('http_ip_address', 'Proxy\'s HTTP IP', properties=('mandatory',))
http_ip_short = SymLinkOption('i', http_ip_address)
http_port = PortOption('http_port', 'Proxy\'s HTTP Port', default='8080', properties=('mandatory',))
http_port_short = SymLinkOption('p', http_port)
manual_proxy = OptionDescription('manual_proxy', 'Manual proxy settings', [http_ip_address, http_ip_short, http_port, http_port_short],
requires=[{'option': proxy_mode, 'expected': 'Manual proxy configuration', 'action':'disabled', 'inverse':True}])
auto_config_url = URLOption('auto_config_url','Proxy\'s auto config URL', properties=('mandatory',))
auto_config_url_short = SymLinkOption('i', auto_config_url)
automatic_proxy = OptionDescription('automatic_proxy', 'Automatic proxy setting',
[auto_config_url, auto_config_url_short],
requires=[{'option': proxy_mode, 'expected': 'Automatic proxy configuration URL', 'action':'disabled', 'inverse': True}])
configuration = OptionDescription('configuration', None,
[manual_proxy, automatic_proxy])
no_proxy_domain = DomainnameOption('no_proxy_domain', 'Domain names for which proxy will be desactivated', multi=True)
no_proxy_network = NetworkOption('no_proxy_network', 'Network addresses', multi=True)
no_proxy_network_short = SymLinkOption('n', no_proxy_network)
no_proxy_netmask = NetmaskOption('no_proxy_netmask', 'Netmask addresses', multi=True, properties=('mandatory',))
no_proxy_network_leadership = Leadership('no_proxy_network', 'Network for which proxy will be desactivated', [no_proxy_network, no_proxy_netmask])
no_proxy = OptionDescription('no_proxy', 'Disabled proxy',
[no_proxy_domain, no_proxy_network_leadership],
requires=[{'option': proxy_mode, 'expected': 'No proxy', 'action':'disabled'}, {'option': proxy_mode, 'expected': None, 'action':'disabled'}])
dns_over_https = BoolOption('dns_over_https', 'Enable DNS over HTTPS', default=False)
root = OptionDescription('proxy', 'Proxy parameters',
[proxy_mode, configuration, no_proxy, dns_over_https])
def display_name(option, dyn_name):
return "--" + option.impl_getpath()
from tiramisu import default_storage
default_storage.setting(engine='sqlite3')
proxy_config = Config(root, display_name=display_name, persistent=True, session_id='proxy')
proxy_config.property.read_write()
from tiramisu_cmdline_parser import TiramisuCmdlineParser
parser = TiramisuCmdlineParser(proxy_config)
parser.parse_args(valid_mandatory=False)
from pprint import pprint
pprint(proxy_config.value.dict())

132
docs/src/quiz.py Normal file
View file

@ -0,0 +1,132 @@
from sys import exit
from tiramisu import (MetaConfig, Config, OptionDescription,
BoolOption, ChoiceOption, StrOption, IntOption,
Calculation, Params, ParamOption,
default_storage, list_sessions)
from tiramisu.error import ConflictError
default_storage.setting(engine="sqlite3")
def verif(q: str, a: str):
return q == a
def results(*verif):
return sum(verif)
questions = [{'description': 'what does the cat say?',
'proposal': ('woof', 'meow'),
'answer': 'meow'},
{'description': 'what do you get by mixing blue and yellow?',
'proposal': ('green', 'red', 'purple'),
'answer': 'green'},
{'description': 'where is Bryan?',
'proposal': ('at school', 'in his bedroom', 'in the kitchen'),
'answer': 'in the kitchen'},
{'description': 'which one has 4 legs and 2 wings?',
'proposal': ('a wyvern', 'a dragon', 'a wyrm', 'a drake'),
'answer': 'a dragon'},
{'description': 'why life?',
'proposal': ('because', 'I don\'t know', 'good question'),
'answer': 'good question'},
]
options_obj = []
results_obj = []
for idx, question in enumerate(questions):
idx += 1
choice = ChoiceOption('question',
question['description'],
question['proposal'])
answer = StrOption('answer',
f'Answer {idx}',
default=question['answer'],
properties=('frozen',))
boolean = BoolOption('verif',
f'Verif of question {idx}',
Calculation(verif,
Params((ParamOption(choice),
ParamOption(answer)))),
properties=('frozen',))
optiondescription = OptionDescription(f'question_{idx}',
f'Question {idx}',
[choice, answer, boolean])
options_obj.append(optiondescription)
results_obj.append(ParamOption(boolean))
options_obj.append(IntOption('res',
'Quiz results',
Calculation(results,
Params(tuple(results_obj)))))
rootod = OptionDescription('root', '', options_obj)
meta_cfg = MetaConfig([], optiondescription=rootod, persistent=True, session_id="quiz")
def run_quiz(meta_cfg: MetaConfig):
pseudo = input("Enter a name: ")
try:
cfg = meta_cfg.config.new(pseudo, persistent=True)
except ConflictError:
print(f'Hey {pseudo} you already answered the questionnaire')
exit()
cfg.property.read_write()
for idx, question in enumerate(cfg.option.list(type='optiondescription')):
question_id = question.option.doc()
question_obj = question.option('question')
question_doc = question_obj.option.doc()
print(f'{question_id}: {question_doc}')
print(*question_obj.value.list(), sep=" | ")
while True:
input_ans = input('Your answer: ')
try:
question_obj.value.set(input_ans)
except ValueError as err:
err.prefix = ''
print(err)
else:
break
if question.option('verif').value.get() is True:
print('Correct answer!')
else:
print("Wrong answer... the correct answer was:", question.option('answer').value.get())
print('')
qno = idx + 1
print("Correct answers:", cfg.option('res').value.get(), "out of", qno)
if cfg.option('res').value.get() == 0 :
print("Ouch... Maybe next time?")
elif cfg.option('res').value.get() == qno :
print("Wow, great job!")
def quiz_results(meta_cfg: MetaConfig):
for cfg in meta_cfg.config.list():
print(f"==================== {cfg.config.name()} ==========================")
for idx, question in enumerate(cfg.option.list(type='optiondescription')):
if question.option('verif').value.get() is True:
answer = "correct answer"
else:
answer = "wrong answer: " + str(question.option('question').value.get())
print(question.option.doc() + ': ' + answer)
qno = idx + 1
print(f'{cfg.config.name()}\'s score: {cfg.option("res").value.get()} out of {qno}')
# reload old sessions
for session_id in list_sessions():
# our meta config is just here to be a base, so we don't want its session id to be used
if session_id != "quiz":
meta_cfg.config.new(session_id, persistent=True)
while True:
who = input("Who are you? (a student | a teacher): ")
if who in ['a student', 'a teacher']:
break
if who == 'a student':
run_quiz(meta_cfg)
else:
quiz_results(meta_cfg)

133
docs/src/validator.py Normal file
View file

@ -0,0 +1,133 @@
#!/usr/bin/env python3
from tiramisu import StrOption, IntOption, OptionDescription, Config, \
Calculation, Params, ParamOption, ParamSelfOption, ParamValue
from tiramisu.error import ValueWarning
import warnings
from re import match
# Creation differents function
def is_password_conform(password):
# password must containe at least a number, a lowercase letter, an uppercase letter and a symbol
if not match(r'(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*\W)', password):
raise ValueError('please choose a stronger password, try a mix of letters, numbers and symbols')
# Password must have at least min_len characters
def password_correct_len(min_len, recommand_len, password):
if len(password) < min_len:
raise ValueError(f'use {min_len} characters or more for your password')
# password should have at least recommand_len characters
if len(password) < recommand_len:
raise ValueWarning(f'it would be better to use more than {recommand_len} characters for your password')
def user_not_in_password(login, password):
if login in password:
raise ValueError('the login must not be part of the password')
def password_match(password1, password2):
if password1 != password2:
raise ValueError("those passwords didn't match, try again")
# Create first option to ask user's login
login = StrOption('login', 'Login', properties=('mandatory',))
# Creation calculatin for first password
calc1 = Calculation(is_password_conform,
Params(ParamSelfOption()))
calc2 = Calculation(password_correct_len,
Params((ParamValue(8),
ParamValue(12),
ParamSelfOption())))
calc3 = Calculation(user_not_in_password,
Params(kwargs={'login': ParamOption(login),
'password': ParamSelfOption()}),
warnings_only=True)
# Create second option to ask user's password
password1 = StrOption('password1',
'Password',
properties=('mandatory',),
validators=[calc1, calc2, calc3])
# Create third option to confirm user's password
password2 = StrOption('password2',
'Confirm',
properties=('mandatory',),
validators=[Calculation(password_match, Params((ParamOption(password1), ParamSelfOption())))])
# Creation optiondescription and config
od = OptionDescription('password', 'Define your password', [password1, password2])
root = OptionDescription('root', '', [login, od])
config = Config(root)
config.property.read_write()
# no number and no symbol (with prefix)
config.option('login').value.set('user')
try:
config.option('password.password1').value.set('aAbBc')
except ValueError as err:
print(f'Error: {err}')
# no number and no symbol
config.option('login').value.set('user')
try:
config.option('password.password1').value.set('aAbBc')
except ValueError as err:
err.prefix = ''
print(f'Error: {err}')
# too short password
config.option('login').value.set('user')
try:
config.option('password.password1').value.set('aZ$1')
except ValueError as err:
err.prefix = ''
print(f'Error: {err}')
# warnings too short password
warnings.simplefilter('always', ValueWarning)
config.option('login').value.set('user')
with warnings.catch_warnings(record=True) as warn:
config.option('password.password1').value.set('aZ$1bN:2')
if warn:
warn[0].message.prefix = ''
print(f'Warning: {warn[0].message}')
password = config.option('password.password1').value.get()
print(f'The password is "{password}"')
# password with login
warnings.simplefilter('always', ValueWarning)
config.option('login').value.set('user')
with warnings.catch_warnings(record=True) as warn:
config.option('password.password1').value.set('aZ$1bN:2u@1Bjuser')
if warn:
warn[0].message.prefix = ''
print(f'Warning: {warn[0].message}')
password = config.option('password.password1').value.get()
print(f'The password is "{password}"')
# password1 not matching password2
config.option('login').value.set('user')
config.option('password.password1').value.set('aZ$1bN:2u@1Bj')
try:
config.option('password.password2').value.set('aZ$1aaaa')
except ValueError as err:
err.prefix = ''
print(f'Error: {err}')
# and finaly passwod match
config.option('login').value.set('user')
config.option('password.password1').value.set('aZ$1bN:2u@1Bj')
config.option('password.password2').value.set('aZ$1bN:2u@1Bj')
config.property.read_only()
user_login = config.option('login').value.get()
password = config.option('password.password2').value.get()
print(f'The password for "{user_login}" is "{password}"')

View file

@ -0,0 +1,41 @@
from tiramisu import StrOption, IntOption, Leadership, OptionDescription, Config, \
Calculation, Params, ParamSelfOption, ParamIndex
from tiramisu.error import ValueWarning
import warnings
def valid_pourcent(option, current_option, index):
if None in option:
return
total = sum(option)
if total > 100:
raise ValueError(f'the value {current_option} (at index {index}) is too big, the total is {total}%')
calculation = Calculation(valid_pourcent, Params((ParamSelfOption(whole=True),
ParamSelfOption(),
ParamIndex())))
user = StrOption('user', 'User', multi=True)
percent = IntOption('percent',
'Distribution',
multi=True,
validators=[calculation])
od = Leadership('percent', 'Percent', [user, percent])
config = Config(OptionDescription('root', 'root', [od]))
config.option('percent.user').value.set(['user1', 'user2'])
config.option('percent.percent', 0).value.set(20)
# too big
try:
config.option('percent.percent', 1).value.set(90)
except ValueError as err:
err.prefix = ''
print(f'Error: {err}')
# correct
config.option('percent.percent', 1).value.set(80)

View file

@ -0,0 +1,44 @@
from tiramisu import IntOption, OptionDescription, Config, \
Calculation, Params, ParamSelfOption
from tiramisu.error import ValueWarning
import warnings
def valid_pourcent(option):
total = sum(option)
if total > 100:
raise ValueError(f'the total {total}% is bigger than 100%')
if total < 100:
raise ValueWarning(f'the total {total}% is lower than 100%')
percent = IntOption('percent',
'Percent',
multi=True,
validators=[Calculation(valid_pourcent, Params(ParamSelfOption()))])
config = Config(OptionDescription('root', 'root', [percent]))
# too big
try:
config.option('percent').value.set([20, 90])
except ValueError as err:
err.prefix = ''
print(f'Error: {err}')
percent_value = config.option('percent').value.get()
print(f'The value is "{percent_value}"')
# too short
warnings.simplefilter('always', ValueWarning)
with warnings.catch_warnings(record=True) as warn:
config.option('percent').value.set([20, 70])
if warn:
warn[0].message.prefix = ''
print(f'Warning: {warn[0].message}')
percent_value = config.option('percent').value.get()
print(f'The value is "{percent_value}"')
# correct
config.option('percent').value.set([20, 80])
percent_value = config.option('percent').value.get()
print(f'The value is "{percent_value}"')

BIN
docs/storage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

265
docs/storage.svg Normal file
View file

@ -0,0 +1,265 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="400"
height="200"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="test.svg"
inkscape:export-filename="/home/gnunux/git/tiramisu/doc/storage.png"
inkscape:export-xdpi="135"
inkscape:export-ydpi="135">
<defs
id="defs4">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 526.18109 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="744.09448 : 526.18109 : 1"
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
id="perspective3827" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="106.95445"
inkscape:cy="208.15932"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1600"
inkscape:window-height="841"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-852.36218)">
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 235.5,78.588237 306,109"
id="path4403"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0"
inkscape:connection-start="#g4211"
inkscape:connection-start-point="d4"
transform="translate(0,852.36218)" />
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 235.5,131.08416 305,107"
id="path4405"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0"
inkscape:connection-start="#g4216"
inkscape:connection-start-point="d4"
transform="translate(0,852.36218)" />
<g
id="g4206"
transform="translate(-17,590)">
<text
sodipodi:linespacing="686.00001%"
id="text2985"
y="368.36218"
x="98"
style="font-size:10px;font-style:normal;font-weight:normal;line-height:686.00001335%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
y="368.36218"
x="98"
id="tspan2987"
sodipodi:role="line">Config</tspan></text>
<rect
y="351.36218"
x="81"
height="30"
width="63"
id="rect3757"
style="fill:none;stroke:#000000;stroke-linejoin:round;stroke-opacity:1" />
</g>
<g
id="g4211"
transform="translate(-17,590)">
<rect
y="312.36218"
x="189.5"
height="30"
width="63"
id="rect3757-2"
style="fill:none;stroke:#000000;stroke-linejoin:round;stroke-opacity:1" />
<text
sodipodi:linespacing="686.00001%"
id="text3777"
y="330.36218"
x="206"
style="font-size:10px;font-style:normal;font-weight:normal;line-height:686.00001335%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
y="330.36218"
x="206"
id="tspan3779"
sodipodi:role="line">Values</tspan></text>
</g>
<g
id="g4216"
transform="translate(-17,590)">
<rect
y="389.36218"
x="189.5"
height="30"
width="63"
id="rect3757-4"
style="fill:none;stroke:#000000;stroke-linejoin:round;stroke-opacity:1" />
<text
sodipodi:linespacing="686.00001%"
id="text3799"
y="407.36218"
x="200"
style="font-size:10px;font-style:normal;font-weight:normal;line-height:686.00001335%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
y="407.36218"
x="200"
id="tspan3801"
sodipodi:role="line">Settings</tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 127,967.39444 45.5,15.93548"
id="path4028"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 127,945.0396 45.5,-16.35484"
id="path4030"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect4161"
width="55.5"
height="26"
x="277.5"
y="946.36218" />
<path
sodipodi:type="arc"
style="fill:none;stroke:#000000;stroke-width:1.96347165;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path3843"
sodipodi:cx="401"
sodipodi:cy="334.86218"
sodipodi:rx="38"
sodipodi:ry="10.5"
d="m 439,334.86218 a 38,10.5 0 1 1 -76,0 38,10.5 0 1 1 76,0 z"
transform="matrix(0.71325325,0,0,0.57998971,18.66254,749.17042)" />
<path
transform="matrix(0.71325325,0,0,0.57998971,18.57337,775.05247)"
sodipodi:type="arc"
style="fill:none;stroke:#000000;stroke-width:1.96347165;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path3843-3"
sodipodi:cx="401"
sodipodi:cy="334.86218"
sodipodi:rx="38"
sodipodi:ry="10.5"
d="m 439,334.86218 a 38,10.5 0 1 1 -76,0 38,10.5 0 1 1 76,0 z" />
<path
transform="matrix(0.71325325,0,0,0.57998971,18.52879,762.07519)"
sodipodi:type="arc"
style="fill:none;stroke:#000000;stroke-width:1.96347165;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="path3843-3-0"
sodipodi:cx="401"
sodipodi:cy="334.86218"
sodipodi:rx="38"
sodipodi:ry="10.5"
d="m 439,334.86218 a 38,10.5 0 1 1 -76,0 38,10.5 0 1 1 76,0 z" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3883"
width="62.989182"
height="6.7061315"
x="274.72043"
y="949.91193" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3883-3"
width="58.087975"
height="6.4161367"
x="277.34818"
y="962.78046" />
<path
style="fill:none;stroke:#000000;stroke-width:1.26286423;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline"
d="m 277.52869,943.35095 -0.0442,26.02673"
id="path3917"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:1.26286423;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline"
d="m 331.64698,969.26909 0.13377,-26.17203"
id="path3921"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0" />
<text
xml:space="preserve"
style="font-size:10px;font-style:normal;font-weight:normal;line-height:686.00001335%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
x="286.33643"
y="958.32324"
id="text3821"
sodipodi:linespacing="686.00001%"><tspan
sodipodi:role="line"
id="tspan3823"
x="286.33643"
y="958.32324">Storage</tspan></text>
<g
id="g4201"
transform="translate(-17,590)">
<rect
y="293.42468"
x="81"
height="30"
width="63"
id="rect3757-5"
style="fill:none;stroke:#000000;stroke-linejoin:round;stroke-opacity:1" />
<text
sodipodi:linespacing="100%"
id="text4190"
y="309.42468"
x="110.27588"
style="font-size:10px;font-style:normal;font-weight:normal;text-align:center;line-height:100%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
id="tspan4194"
y="309.42468"
x="110.27588"
sodipodi:role="line">Option</tspan></text>
</g>
<path
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 95.5,913.42468 0,27.9375"
id="path4199"
inkscape:connector-type="polyline"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

13
docs/symlinkoption.rst Normal file
View file

@ -0,0 +1,13 @@
====================================================
The symbolic link option: :class:`SymLinkOption`
====================================================
A `SymLinkOption` is an option that actually points to another option.
Each time we will access to a properties of this options, we will have in return the value of other option.
Creation a `SymLinkOption` is easy:
>>> from tiramisu import StrOption, SymLinkOption
>>> st = StrOption('str', 'str')
>>> sym = SymLinkOption('sym', st)

271
docs/validator.rst Normal file
View file

@ -0,0 +1,271 @@
==================================
Validator
==================================
What are validator?
==================================
Validator is a functionnality that allow you to call a function to determine if an option is valid.
To define validator we have to use :doc:`calculation` object.
The function have to raise a `ValueError` object if the value is not valid. It could emit a warning when raises a `ValueWarning`.
Validator with options
=============================
Here an example, where we want to ask a new password to an user. This password should not be weak. The password will be asked twice and must match.
First of all, import necessary object:
.. literalinclude:: src/validator.py
:lines: 3-7
:linenos:
Create a first function to valid that the password is not weak:
.. literalinclude:: src/validator.py
:lines: 10-14
:linenos:
Secondly create a function to valid password length. The password must be longer than the value of `min_len` and should be longer than the value of `recommand_len`.
In first case, function raise `ValueError`, this value is incorrect.
In second case, function raise `ValueWarning`, the value is valid but discouraged:
.. literalinclude:: src/validator.py
:lines: 17-23
:linenos:
Thirdly create a function that verify that the login name is not a part of password (password `foo2aZ$` if not valid for user `foo`):
.. literalinclude:: src/validator.py
:lines: 26-28
:linenos:
Now we can creation an option to ask user login:
.. literalinclude:: src/validator.py
:lines: 36-37
:linenos:
Create a calculation to launch `is_password_conform`. This function will be use in a new option and must validate this new option. So we use the object `ParamSelfOption` has parameter to retrieve the value of current option:
.. literalinclude:: src/validator.py
:lines: 39-41
:linenos:
Create a second calculation to launch `password_correct_len` function. We want set 8 as `min_len` value and 12 as `recommand_len` value:
.. literalinclude:: src/validator.py
:lines: 43-46
:linenos:
Create a third calculation to launch `user_not_in_password` function. For this function, we use keyword argument. This function normaly raise `ValueError` but in this case we want demoting this error as a simple warning. So we add `warnings_only` parameter:
.. literalinclude:: src/validator.py
:lines: 48-51
:linenos:
So now we can create first password option that use those calculations:
.. literalinclude:: src/validator.py
:lines: 54-58
:linenos:
A new function is created to conform that password1 and password2 match:
.. literalinclude:: src/validator.py
:lines: 31-33
:linenos:
And now we can create second password option that use this function:
.. literalinclude:: src/validator.py
:lines: 60-64
:linenos:
Finally we create optiondescription and config:
.. literalinclude:: src/validator.py
:lines: 66-70
:linenos:
Now we can test this `Config`:
.. literalinclude:: src/validator.py
:lines: 72-77
:linenos:
The tested password is too weak, so value is not set.
The error is: `Error: "aAbBc" is an invalid string for "Password", please choose a stronger password, try a mix of letters, numbers and symbols`.
The password is part of error message. In this case it's a bad idea. So we have to remove `prefix` to the error message:
.. literalinclude:: src/validator.py
:lines: 79-85
:linenos:
Now the error is: `Error: please choose a stronger password, try a mix of letters, numbers and symbols`.
Let's try with a password not weak but too short:
.. literalinclude:: src/validator.py
:lines: 87-93
:linenos:
The error is: `Error: use 8 characters or more for your password`.
Now try a password with 8 characters:
.. literalinclude:: src/validator.py
:lines: 95-104
:linenos:
Warning is display but password is store:
`Warning: it would be better to use more than 12 characters for your password`
`The password is "aZ$1bN:2"`
Try a password with the login as part of it:
.. literalinclude:: src/validator.py
:lines: 106-115
:linenos:
Warning is display but password is store:
`Warning: the login must not be part of the password`
`The password is "aZ$1bN:2u@1Bjuser"`
Now try with a valid password but that doesn't match:
.. literalinclude:: src/validator.py
:lines: 117-124
:linenos:
An error is displayed: `Error: those passwords didn't match, try again`.
Finally try a valid password:
.. literalinclude:: src/validator.py
:lines: 126-133
:linenos:
As expected, we have `The password for "user" is "aZ$1bN:2u@1Bj"`.
Validator with a multi option
================================
Assume we ask percentage value to an user. The sum of values mustn't be higher than 100% and shouldn't be lower than 100%.
Let's start by importing the objects:
.. literalinclude:: src/validator_multi.py
:lines: 1-4
:linenos:
Continue by writing the validation function:
.. literalinclude:: src/validator_multi.py
:lines: 7-12
:linenos:
And create a simple config:
.. literalinclude:: src/validator_multi.py
:lines: 15-19
:linenos:
Now try with bigger sum:
.. literalinclude:: src/validator_multi.py
:lines: 22-29
:linenos:
The result is:
`Error: the total 110% is bigger than 100%`
`The value is "[]"`
Let's try with lower sum:
.. literalinclude:: src/validator_multi.py
:lines: 31-39
:linenos:
The result is:
`Warning: the total 90% is lower than 100%`
`The value is "[20, 70]"`
Finally with correct value:
.. literalinclude:: src/validator_multi.py
:lines: 41-44
:linenos:
The result is:
`The value is "[20, 80]"`
Validator with a follower option
==================================
Assume we want distribute something to differents users. The sum of values mustn't be higher than 100%.
First, import all needed objects:
.. literalinclude:: src/validator_follower.py
:lines: 1-4
:linenos:
Let's start to write a function with three arguments:
- the first argument will have all values set for the follower
- the second argument will have only last value set for the follower
- the third argument will have the index
.. literalinclude:: src/validator_follower.py
:lines: 7-12
:linenos:
Continue by creating a calculation:
.. literalinclude:: src/validator_follower.py
:lines: 15-17
:linenos:
And instanciate differents option and config:
.. literalinclude:: src/validator_follower.py
:lines: 20-26
:linenos:
Add two value to the leader:
.. literalinclude:: src/validator_follower.py
:lines: 29
:linenos:
The user user1 will have 20%:
.. literalinclude:: src/validator_follower.py
:lines: 30
:linenos:
If we try to set 90% to user2:
.. literalinclude:: src/validator_follower.py
:lines: 33-38
:linenos:
This error occured: `Error: the value 90 (at index 1) is too big, the total is 110%`
No problem with 80%:
.. literalinclude:: src/validator_follower.py
:lines: 40-41
:linenos:

File diff suppressed because it is too large Load diff

823
locale/tiramisu.pot Normal file
View file

@ -0,0 +1,823 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2024-11-05 08:52+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
#: tiramisu/api.py:79
msgid "Settings:"
msgstr ""
#: tiramisu/api.py:83
msgid "Access to option without verifying permissive properties"
msgstr ""
#: tiramisu/api.py:88
msgid "Access to option without property restriction"
msgstr ""
#: tiramisu/api.py:93
msgid "Do not warnings during validation"
msgstr ""
#: tiramisu/api.py:97
msgid "Commands:"
msgstr ""
#: tiramisu/api.py:111 tiramisu/api.py:1840
msgid "please specify a valid sub function ({0}.{1})"
msgstr ""
#: tiramisu/api.py:194
msgid "please do not specify index ({0}.{1})"
msgstr ""
#: tiramisu/api.py:199 tiramisu/api.py:844
msgid "please specify index with a follower option ({0}.{1})"
msgstr ""
#: tiramisu/api.py:220
msgid "please specify a valid sub function ({0}.{1}): {2}"
msgstr ""
#: tiramisu/api.py:431
msgid "the option {0} is not a dynamic option, cannot get identifiers with only_self parameter to True"
msgstr ""
#: tiramisu/api.py:517
msgid "cannot get option from a follower symlink without index"
msgstr ""
#: tiramisu/api.py:592
msgid "cannot add this property: \"{0}\""
msgstr ""
#: tiramisu/api.py:619
msgid "cannot remove option's property \"{0}\", use permissive instead in option \"{1}\""
msgstr ""
#: tiramisu/api.py:623
msgid "cannot find \"{0}\" in option \"{1}\""
msgstr ""
#: tiramisu/api.py:628
msgid "cannot remove option's property \"{0}\", use permissive instead in option \"{1}\" at index \"{2}\""
msgstr ""
#: tiramisu/api.py:632
msgid "cannot find \"{0}\" in option \"{1}\" at index \"{2}\""
msgstr ""
#: tiramisu/api.py:676
msgid "cannot find \"{0}\""
msgstr ""
#: tiramisu/api.py:808
msgid "cannot reduce length of the leader {}"
msgstr ""
#: tiramisu/api.py:861
msgid "only multi value has defaultmulti"
msgstr ""
#: tiramisu/api.py:1020
msgid "please specify a valid sub function ({0}.{1}) for {2}"
msgstr ""
#: tiramisu/api.py:1407
msgid "properties must be a frozenset"
msgstr ""
#: tiramisu/api.py:1411 tiramisu/api.py:1438
msgid "unknown when {} (must be in append or remove)"
msgstr ""
#: tiramisu/api.py:1424 tiramisu/api.py:1448 tiramisu/config.py:1680
msgid "unknown type {}"
msgstr ""
#: tiramisu/api.py:1812
msgid "do not use unrestraint, nowarnings or forcepermissive together"
msgstr ""
#: tiramisu/autolib.py:80
msgid "args in params must be a tuple"
msgstr ""
#: tiramisu/autolib.py:83 tiramisu/autolib.py:88
msgid "arg in params must be a Param"
msgstr ""
#: tiramisu/autolib.py:85
msgid "kwargs in params must be a dict"
msgstr ""
#: tiramisu/autolib.py:113
msgid "paramoption needs an option not {}"
msgstr ""
#: tiramisu/autolib.py:119
msgid "param must have a boolean not a {} for notraisepropertyerror"
msgstr ""
#: tiramisu/autolib.py:122
msgid "param must have a boolean not a {} for raisepropertyerror"
msgstr ""
#: tiramisu/autolib.py:212
msgid "option in ParamInformation cannot be a symlinkoption"
msgstr ""
#: tiramisu/autolib.py:215
msgid "option in ParamInformation cannot be a follower"
msgstr ""
#: tiramisu/autolib.py:218
msgid "option in ParamInformation cannot be a dynamic option"
msgstr ""
#: tiramisu/autolib.py:279
msgid "first argument ({0}) must be a function"
msgstr ""
#: tiramisu/autolib.py:283
msgid "help_function ({0}) must be a function"
msgstr ""
#: tiramisu/autolib.py:452 tiramisu/autolib.py:514
msgid "unable to carry out a calculation for {}, {}"
msgstr ""
#: tiramisu/autolib.py:461 tiramisu/autolib.py:521
msgid "the option {0} is used in a calculation but is invalid ({1})"
msgstr ""
#: tiramisu/autolib.py:477 tiramisu/autolib.py:535 tiramisu/autolib.py:584
msgid "unable to get value for calculating {0}, {1}"
msgstr ""
#: tiramisu/autolib.py:601
msgid "option {0} is not a dynoptiondescription or in a dynoptiondescription"
msgstr ""
#: tiramisu/autolib.py:848
msgid "the \"{}\" function with positional arguments \"{}\" and keyword arguments \"{}\" must not return a list (\"{}\") for the follower option {}"
msgstr ""
#: tiramisu/autolib.py:863
msgid "the \"{}\" function must not return a list (\"{}\") for the follower option {}"
msgstr ""
#: tiramisu/autolib.py:904
msgid "unexpected error \"{0}\" in function \"{1}\" with arguments \"{3}\" and \"{4}\" for option {2}"
msgstr ""
#: tiramisu/autolib.py:915
msgid "unexpected error \"{0}\" in function \"{1}\" for option {2}"
msgstr ""
#: tiramisu/config.py:419
msgid "index \"{0}\" is greater than the leadership length \"{1}\" for option {2}"
msgstr ""
#: tiramisu/config.py:579
msgid "there is no option description for this config (may be GroupConfig)"
msgstr ""
#: tiramisu/config.py:668
msgid "no option found in config with these criteria"
msgstr ""
#: tiramisu/config.py:871
msgid "the follower option {0} has greater length ({1}) than the leader length ({2})"
msgstr ""
#: tiramisu/config.py:982 tiramisu/option/optiondescription.py:74
msgid "option description seems to be part of an other config"
msgstr ""
#: tiramisu/config.py:1144
msgid "parent of {0} not already exists"
msgstr ""
#: tiramisu/config.py:1191
msgid "cannot set leadership object has root optiondescription"
msgstr ""
#: tiramisu/config.py:1194
msgid "cannot set dynoptiondescription object has root optiondescription"
msgstr ""
#: tiramisu/config.py:1246
msgid "config name must be uniq in groupconfig for \"{0}\""
msgstr ""
#: tiramisu/config.py:1457
msgid "unknown config \"{}\""
msgstr ""
#: tiramisu/config.py:1482
msgid "child must be a Config, MixConfig or MetaConfig"
msgstr ""
#: tiramisu/config.py:1517
msgid "force_default, force_default_if_same or force_dont_change_value cannot be set with only_config"
msgstr ""
#: tiramisu/config.py:1527
msgid "force_default and force_dont_change_value cannot be set together"
msgstr ""
#: tiramisu/config.py:1676
msgid "config name must be uniq in groupconfig for {0}"
msgstr ""
#: tiramisu/config.py:1721
msgid "config added has no name, the name is mandatory"
msgstr ""
#: tiramisu/config.py:1726
msgid "config name \"{0}\" is not uniq in groupconfig \"{1}\""
msgstr ""
#: tiramisu/config.py:1744 tiramisu/config.py:1750
msgid "cannot find the config {0}"
msgstr ""
#: tiramisu/config.py:1776
msgid "MetaConfig with optiondescription must have string has child, not {}"
msgstr ""
#: tiramisu/config.py:1788
msgid "child must be a Config or MetaConfig"
msgstr ""
#: tiramisu/config.py:1793
msgid "all config in metaconfig must have the same optiondescription"
msgstr ""
#: tiramisu/config.py:1810
msgid "metaconfig must have the same optiondescription"
msgstr ""
#: tiramisu/error.py:31
msgid "and"
msgstr ""
#: tiramisu/error.py:33
msgid "or"
msgstr ""
#: tiramisu/error.py:55
msgid " {} "
msgstr ""
#: tiramisu/error.py:108
msgid "property"
msgstr ""
#: tiramisu/error.py:110
msgid "properties"
msgstr ""
#: tiramisu/error.py:113
msgid "cannot modify the {0} {1} because \"{2}\" has {3} {4}"
msgstr ""
#: tiramisu/error.py:115
msgid "cannot modify the {0} {1} because has {2} {3}"
msgstr ""
#: tiramisu/error.py:118
msgid "cannot access to {0} {1} because \"{2}\" has {3} {4}"
msgstr ""
#: tiramisu/error.py:120
msgid "cannot access to {0} {1} because has {2} {3}"
msgstr ""
#: tiramisu/error.py:192
msgid "invalid value"
msgstr ""
#: tiramisu/error.py:201
msgid "attention, \"{0}\" could be an invalid {1} for \"{2}\""
msgstr ""
#: tiramisu/error.py:219 tiramisu/error.py:228
msgid "\"{0}\" is an invalid {1} for \"{2}\""
msgstr ""
#: tiramisu/function.py:65
msgid "network \"{0}\" ({1}) does not match with this netmask"
msgstr ""
#: tiramisu/function.py:83
msgid "IP \"{0}\" ({1}) with this netmask is in fact a network address"
msgstr ""
#: tiramisu/function.py:88
msgid "IP \"{0}\" ({1}) with this netmask is in fact a broadcast address"
msgstr ""
#: tiramisu/function.py:106
msgid "broadcast invalid with network {0} ({1}) and netmask {2} ({3})"
msgstr ""
#: tiramisu/function.py:134
msgid "this IP is not in network {network[\"value\"]} ({network[\"name\"]})"
msgstr ""
#: tiramisu/function.py:136
msgid "this IP is not in network {network[\"value\"]} ({network[\"name\"]}) with netmask {netmask[\"value\"]} ({netmask[\"name\"]})"
msgstr ""
#: tiramisu/function.py:143
msgid "this IP with the network {0} ({1}) is in fact a network address"
msgstr ""
#: tiramisu/function.py:148
msgid "this IP with the network {0} ({1}) is in fact a broadcast address"
msgstr ""
#: tiramisu/function.py:165
msgid "value is identical to {0}"
msgstr ""
#: tiramisu/function.py:400
msgid "unexpected value in calc_value with join attribute \"{0}\" with invalid length \"{1}\""
msgstr ""
#: tiramisu/function.py:527
msgid "unexpected {0} condition_operator in calc_value"
msgstr ""
#: tiramisu/function.py:591
msgid "unexpected condition_{0} must have \"todict\" argument"
msgstr ""
#: tiramisu/function.py:602
msgid "the value of \"{0}\" is {1}"
msgstr ""
#: tiramisu/function.py:604
msgid "the value of \"{0}\" is not {1}"
msgstr ""
#: tiramisu/option/baseoption.py:75 tiramisu/option/symlinkoption.py:44
msgid "\"{0}\" is an invalid name for an option"
msgstr ""
#: tiramisu/option/baseoption.py:88
msgid "invalid properties type {0} for {1}, must be a frozenset"
msgstr ""
#: tiramisu/option/baseoption.py:98
msgid "invalid property type {0} for {1}, must be a string or a Calculation"
msgstr ""
#: tiramisu/option/baseoption.py:249
msgid "information's item for {0} not found: \"{1}\""
msgstr ""
#: tiramisu/option/baseoption.py:267
msgid "'{0}' ({1}) object attribute '{2}' is read-only"
msgstr ""
#: tiramisu/option/baseoption.py:308
msgid "\"{}\" ({}) object attribute \"{}\" is read-only"
msgstr ""
#: tiramisu/option/baseoption.py:320
msgid "{0} not part of any Config"
msgstr ""
#: tiramisu/option/broadcastoption.py:41
msgid "invalid string"
msgstr ""
#: tiramisu/option/choiceoption.py:47
msgid "values must be a tuple or a calculation for {0}"
msgstr ""
#: tiramisu/option/choiceoption.py:70
msgid "the calculated values \"{0}\" for \"{1}\" is not a list"
msgstr ""
#: tiramisu/option/choiceoption.py:101
msgid "only \"{0}\" is allowed"
msgstr ""
#: tiramisu/option/choiceoption.py:103
msgid "only {0} are allowed"
msgstr ""
#: tiramisu/option/domainnameoption.py:60
msgid "unknown type {0} for hostname"
msgstr ""
#: tiramisu/option/domainnameoption.py:63
msgid "allow_ip must be a boolean"
msgstr ""
#: tiramisu/option/domainnameoption.py:65
msgid "allow_cidr_network must be a boolean"
msgstr ""
#: tiramisu/option/domainnameoption.py:67
msgid "allow_without_dot must be a boolean"
msgstr ""
#: tiramisu/option/domainnameoption.py:69
msgid "allow_startswith_dot must be a boolean"
msgstr ""
#: tiramisu/option/domainnameoption.py:81
msgid "must start with lowercase characters followed by lowercase characters, number, \"-\" and \".\" characters are allowed"
msgstr ""
#: tiramisu/option/domainnameoption.py:84
msgid "must start with lowercase characters followed by lowercase characters, number, \"-\" and \".\" characters are recommanded"
msgstr ""
#: tiramisu/option/domainnameoption.py:88
#: tiramisu/option/domainnameoption.py:89
msgid "could be a IP, otherwise {}"
msgstr ""
#: tiramisu/option/domainnameoption.py:134
msgid "invalid length (min 1)"
msgstr ""
#: tiramisu/option/domainnameoption.py:137
msgid "invalid length (max {0})"
msgstr ""
#: tiramisu/option/domainnameoption.py:143
msgid "must have dot"
msgstr ""
#: tiramisu/option/domainnameoption.py:145
msgid "invalid length (max 255)"
msgstr ""
#: tiramisu/option/domainnameoption.py:163
msgid "must not be an IP"
msgstr ""
#: tiramisu/option/domainnameoption.py:186
msgid "some characters are uppercase"
msgstr ""
#: tiramisu/option/dynoptiondescription.py:131
msgid "DynOptionDescription identifiers for option {0}, is not a list ({1})"
msgstr ""
#: tiramisu/option/dynoptiondescription.py:142
msgid "invalid identifier \"{}\" for option {}"
msgstr ""
#: tiramisu/option/dynoptiondescription.py:150
msgid "DynOptionDescription \"{0}\" identifiers return a list with same values \"{1}\""
msgstr ""
#: tiramisu/option/filenameoption.py:47
msgid "types parameter must be a list, not \"{0}\" for \"{1}\""
msgstr ""
#: tiramisu/option/filenameoption.py:67
msgid "must starts with \"/\""
msgstr ""
#: tiramisu/option/filenameoption.py:78
msgid "cannot find {0} \"{1}\""
msgstr ""
#: tiramisu/option/intoption.py:52
msgid "value should be equal or greater than \"{0}\""
msgstr ""
#: tiramisu/option/intoption.py:54
msgid "value must be equal or greater than \"{0}\""
msgstr ""
#: tiramisu/option/intoption.py:59
msgid "value should be less than \"{0}\""
msgstr ""
#: tiramisu/option/intoption.py:61
msgid "value must be less than \"{0}\""
msgstr ""
#: tiramisu/option/ipoption.py:57
msgid "it's in fact a network address"
msgstr ""
#: tiramisu/option/ipoption.py:59
msgid "it's in fact a broacast address"
msgstr ""
#: tiramisu/option/ipoption.py:71
msgid "CIDR address must have a \"/\""
msgstr ""
#: tiramisu/option/ipoption.py:80
msgid "shouldn't be reserved IP"
msgstr ""
#: tiramisu/option/ipoption.py:82
msgid "mustn't be reserved IP"
msgstr ""
#: tiramisu/option/ipoption.py:86
msgid "should be private IP"
msgstr ""
#: tiramisu/option/ipoption.py:88
msgid "must be private IP"
msgstr ""
#: tiramisu/option/leadership.py:55
msgid "cannot set \"group_type\" attribute for a Leadership"
msgstr ""
#: tiramisu/option/leadership.py:67
msgid "a leader and a follower are mandatories in leadership \"{}\""
msgstr ""
#: tiramisu/option/leadership.py:89
msgid "leader cannot have \"{}\" property"
msgstr ""
#: tiramisu/option/leadership.py:101
msgid "leadership {0} shall not have a symlinkoption"
msgstr ""
#: tiramisu/option/leadership.py:108
msgid "leadership {0} shall not have a subgroup"
msgstr ""
#: tiramisu/option/leadership.py:114
msgid "only multi option allowed in leadership {0} but option {1} is not a multi"
msgstr ""
#: tiramisu/option/leadership.py:141
msgid "not allowed default value for follower option {0} in leadership {1}"
msgstr ""
#: tiramisu/option/networkoption.py:45
msgid "must use CIDR notation"
msgstr ""
#: tiramisu/option/networkoption.py:60
msgid "shouldn't be reserved network"
msgstr ""
#: tiramisu/option/networkoption.py:62
msgid "mustn't be reserved network"
msgstr ""
#: tiramisu/option/option.py:73
msgid "default_multi is set whereas multi is False in option: {0}"
msgstr ""
#: tiramisu/option/option.py:93
msgid "invalid multi type \"{}\" for \"{}\""
msgstr ""
#: tiramisu/option/option.py:112
msgid "validators must be a list of Calculation for \"{0}\""
msgstr ""
#: tiramisu/option/option.py:117
msgid "validators must be a Calculation for \"{0}\""
msgstr ""
#: tiramisu/option/option.py:146
msgid "invalid default_multi value \"{0}\" for option {1}"
msgstr ""
#: tiramisu/option/option.py:154
msgid "invalid default_multi value \"{0}\" for option {1}, {2}"
msgstr ""
#: tiramisu/option/option.py:167
msgid "invalid default_multi value \"{0}\" for option {1}, must be a list for a submulti"
msgstr ""
#: tiramisu/option/option.py:290
msgid "the value \"{}\" is not unique"
msgstr ""
#: tiramisu/option/option.py:352
msgid "which must not be a list"
msgstr ""
#: tiramisu/option/option.py:404 tiramisu/option/option.py:430
msgid "which must be a list"
msgstr ""
#: tiramisu/option/option.py:424
msgid "which \"{}\" must be a list of list"
msgstr ""
#: tiramisu/option/optiondescription.py:109
msgid "duplicate option: {0}"
msgstr ""
#: tiramisu/option/optiondescription.py:244
msgid "unknown option \"{0}\" in root optiondescription (it's a dynamic option)"
msgstr ""
#: tiramisu/option/optiondescription.py:279
msgid "unknown option \"{0}\" in root optiondescription"
msgstr ""
#: tiramisu/option/optiondescription.py:282
msgid "unknown option \"{0}\" in optiondescription {1}"
msgstr ""
#: tiramisu/option/optiondescription.py:338
msgid "children in optiondescription \"{}\" must be a list"
msgstr ""
#: tiramisu/option/optiondescription.py:366
msgid "duplicate option name: \"{0}\""
msgstr ""
#: tiramisu/option/optiondescription.py:372
msgid "the option's name \"{0}\" start as the dynoptiondescription's name \"{1}\""
msgstr ""
#: tiramisu/option/optiondescription.py:415
msgid "cannot change group_type if already set (old {0}, new {1})"
msgstr ""
#: tiramisu/option/optiondescription.py:420
msgid "group_type: {0} not allowed"
msgstr ""
#: tiramisu/option/passwordoption.py:49
msgid "at least {0} characters are required"
msgstr ""
#: tiramisu/option/passwordoption.py:52
msgid "maximum {0} characters required"
msgstr ""
#: tiramisu/option/passwordoption.py:57
msgid "must not have the characters {0}"
msgstr ""
#: tiramisu/option/permissionsoption.py:52
msgid "only 3 or 4 octal digits are allowed"
msgstr ""
#: tiramisu/option/permissionsoption.py:63
msgid "user"
msgstr ""
#: tiramisu/option/permissionsoption.py:64
#: tiramisu/option/permissionsoption.py:66
msgid "group"
msgstr ""
#: tiramisu/option/permissionsoption.py:67
msgid "other"
msgstr ""
#: tiramisu/option/permissionsoption.py:68
msgid "{0} has more right than {1}"
msgstr ""
#: tiramisu/option/permissionsoption.py:71
msgid "too weak"
msgstr ""
#: tiramisu/option/portoption.py:74
msgid "inconsistency in allowed range"
msgstr ""
#: tiramisu/option/portoption.py:79
msgid "max value is empty"
msgstr ""
#: tiramisu/option/portoption.py:92
msgid "range must have two values only"
msgstr ""
#: tiramisu/option/portoption.py:95
msgid "first port in range must be smaller than the second one"
msgstr ""
#: tiramisu/option/portoption.py:121
msgid "should be between {0} and {1}"
msgstr ""
#: tiramisu/option/portoption.py:123
msgid "must be between {0} and {1}"
msgstr ""
#: tiramisu/option/symlinkoption.py:51
msgid "malformed symlink second parameters must be an option for \"{0}\", not {1}"
msgstr ""
#: tiramisu/option/urloption.py:91
msgid "must start with http:// or https://"
msgstr ""
#: tiramisu/option/urloption.py:119
msgid "must ends with a valid resource name"
msgstr ""
#: tiramisu/setting.py:255
msgid "can't rebind {0}"
msgstr ""
#: tiramisu/setting.py:262
msgid "can't unbind {0}"
msgstr ""
#: tiramisu/setting.py:464
msgid "invalid property type {type(new_prop)} for {subconfig.option.impl_getname()} with {prop.function.__name__} function"
msgstr ""
#: tiramisu/setting.py:476
msgid "leader cannot have \"{new_prop}\" property"
msgstr ""
#: tiramisu/setting.py:564
msgid "leader cannot have \"{0}\" property"
msgstr ""
#: tiramisu/setting.py:573
msgid "a leader ({0}) cannot have \"force_default_on_freeze\" or \"force_metaconfig_on_freeze\" property without \"frozen\""
msgstr ""
#: tiramisu/setting.py:607
msgid "permissive must be a frozenset"
msgstr ""
#: tiramisu/setting.py:617
msgid "cannot add those permissives: {0}"
msgstr ""
#: tiramisu/setting.py:654
msgid "can't reset properties to the symlinkoption \"{}\""
msgstr ""
#: tiramisu/setting.py:667
msgid "can't reset permissives to the symlinkoption \"{}\""
msgstr ""
#: tiramisu/todict.py:395
msgid "option {} only works when remotable is not \"none\""
msgstr ""
#: tiramisu/todict.py:561
msgid "unable to transform tiramisu object to dict: {}"
msgstr ""
#: tiramisu/todict.py:876 tiramisu/todict.py:1033
msgid "unknown form {}"
msgstr ""
#: tiramisu/todict.py:923
msgid "not in current area"
msgstr ""
#: tiramisu/todict.py:947
msgid "only multi option can have action \"add\", but \"{}\" is not a multi"
msgstr ""
#: tiramisu/todict.py:953
msgid "unknown action {}"
msgstr ""
#: tiramisu/value.py:564 tiramisu/value.py:861
msgid "set owner \"{0}\" is forbidden"
msgstr ""
#: tiramisu/value.py:571
msgid "\"{0}\" is a default value, so we cannot change owner to \"{1}\""
msgstr ""
#: tiramisu/value.py:740
msgid "index {index} is greater than the length {length} for option {subconfig.option.impl_get_display_name(with_quote=True)}"
msgstr ""
#: tiramisu/value.py:847
msgid "information's item not found \"{}\""
msgstr ""

37
pyproject.toml Normal file
View file

@ -0,0 +1,37 @@
[build-system]
build-backend = "flit_core.buildapi"
requires = ["flit_core >=3.8.0,<4"]
[project]
name = "tiramisu"
version = "5.1.0"
authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}]
readme = "README.md"
description = "an options controller tool"
requires-python = ">=3.8"
license = {file = "LICENSE"}
classifiers = [
"License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
"Programming Language :: Python",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
"Natural Language :: English",
"Natural Language :: French",
]
[project.urls]
Home = "https://forge.cloud.silique.fr/stove/tiramisu"
[tool.commitizen]
name = "cz_conventional_commits"
tag_format = "$version"
version_scheme = "pep440"
version_provider = "pep621"
#update_changelog_on_bump = true
changelog_merge_prerelease = true

View file

@ -1,51 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from setuptools import setup
import os
from tiramisu import __version__
ORI_PACKAGE_NAME = 'tiramisu'
PACKAGE_NAME = os.environ.get('PACKAGE_DST', ORI_PACKAGE_NAME)
if PACKAGE_NAME != ORI_PACKAGE_NAME:
package_dir = {PACKAGE_NAME: ORI_PACKAGE_NAME}
else:
package_dir = None
setup(
version=__version__,
author="Tiramisu's team",
author_email='gnunux@gnunux.info',
name=PACKAGE_NAME,
description='an options controller tool',
url='https://framagit.org/tiramisu/tiramisu',
license='GNU Library or Lesser General Public License (LGPL)',
provides=['tiramisu_api'],
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
"Operating System :: OS Independent",
"Natural Language :: English",
"Natural Language :: French",
],
long_description="""\
An options controller tool
-------------------------------------
Due to more and more available options required to set up an operating system,
compiler options or whatever, it became quite annoying to hand the necessary
options to where they are actually used and even more annoying to add new
options. To circumvent these problems the configuration control was
introduced...
Tiramisu is an options handler and an options controller, wich aims at
producing flexible and fast options access.
This version requires Python 3.5 or later.
""",
include_package_data=True,
package_dir=package_dir,
packages=[PACKAGE_NAME],
)

File diff suppressed because it is too large Load diff

View file

@ -42,3 +42,24 @@ def global_owner(config, config_type):
@pytest.fixture(params=PARAMS)
def config_type(request):
return request.param
def parse_od_get(dico):
ret = {}
for k, v in dico.items():
if k.isoptiondescription():
if k.isleadership():
leader_path = k.leader().path()
ret_leadership = []
for variable, value in v.items():
if variable.path() == leader_path:
for val in value:
ret_leadership.append({leader_path: val})
else:
ret_leadership[variable.index()][variable.path()] = value
ret[leader_path] = ret_leadership
else:
ret.update(parse_od_get(v))
else:
ret[k.path()] = v
return ret

View file

@ -1,3 +0,0 @@
{
"options.unicode": null
}

View file

@ -32,6 +32,9 @@ def list_data(ext='.py'):
for filename in filenames:
# if filename.endswith(ext) and not filename.startswith('__'):
if filename.endswith(ext) and not filename.startswith('__') and not 'not_equal' in filename and not 'callback' in filename and not filename == 'unicode2_copy.py' and not filename == 'unicode2_multi_copy.py':
# if 'leadership' in filename:
# print('FIXME')
# continue
ret.append(filename)
return ret
@ -58,7 +61,7 @@ def load_config(filename,
form.extend(mod.get_form(add_extra_od))
config.property.read_write()
if root is None:
values = loads(dumps(config.option.dict(remotable=remote, clearable=clearable, form=form)))
values = loads(dumps(config.dict(remotable=remote, clearable=clearable, form=form)))
else:
values = loads(dumps(config.option(root).dict(remotable=remote, clearable=clearable, form=form)))
return values
@ -380,11 +383,12 @@ def test_updates(filename_mod):
if dico_ori is None:
if clearable == 'minimum' and remote == 'minimum':
with open(join(datadir, modulepath + '.dict'), 'w') as fh:
dump(config.value.dict(), fh, indent=2)
pouet
dump(config.value.get(), fh, indent=2)
else:
assert config.value.dict() == dico_ori, "clearable {}, remote: {}, filename: {}".format(clearable, remote, filename_mod)
assert config.value.get() == dico_ori, "clearable {}, remote: {}, filename: {}".format(clearable, remote, filename_mod)
if root is None:
suboption = config.option
suboption = config
else:
suboption = config.option(root)
if with_model:

View file

@ -54,12 +54,13 @@ def test_cache_importation():
od1 = make_description()
cfg = Config(od1)
cfg.option('u2').value.set(1)
values = cfg._config_bag.context._impl_values_cache
export = cfg.value.exportation()
assert cfg.value.dict() == {'u1': [], 'u2': 1, 'u3': []}
compare(values.get_cached(), {'u2': {None: (1, None)}})
cfg.option('u2').value.set(2)
assert cfg.value.dict() == {'u1': [], 'u2': 2, 'u3': []}
compare(values.get_cached(), {'u2': {None: (2, None)}})
cfg.value.importation(export)
assert cfg.value.dict() == {'u1': [], 'u2': 1, 'u3': []}
compare(values.get_cached(), {})
# assert not list_sessions()
@ -80,10 +81,10 @@ def test_cache_importation_property():
def test_cache_importation_permissive():
od1 = make_description()
cfg = Config(od1)
cfg.option('u2').permissive.set(frozenset(['prop']))
cfg.option('u2').permissive.add('prop')
export = cfg.permissive.exportation()
assert cfg.option('u2').permissive.get() == {'prop'}
cfg.option('u2').permissive.set(frozenset(['prop', 'prop2']))
cfg.option('u2').permissive.add('prop2')
assert cfg.option('u2').permissive.get() == {'prop', 'prop2'}
cfg.permissive.importation(export)
assert cfg.option('u2').permissive.get() == {'prop'}
@ -265,7 +266,7 @@ def test_cache_leadership():
#assert cache['ip_admin_eth0.netmask_admin_eth0'][None][0] == [None]
#assert cache['ip_admin_eth0.netmask_admin_eth0'][0][0] is None
cache = settings.get_cached()
assert set(cache.keys()) == set([None, 'ip_admin_eth0', 'ip_admin_eth0.ip_admin_eth0', 'ip_admin_eth0.netmask_admin_eth0'])
assert set(cache.keys()) == set(['ip_admin_eth0', 'ip_admin_eth0.ip_admin_eth0', 'ip_admin_eth0.netmask_admin_eth0'])
assert set(cache['ip_admin_eth0'].keys()) == set([None])
assert set(cache['ip_admin_eth0.ip_admin_eth0'].keys()) == set([None])
assert set(cache['ip_admin_eth0.netmask_admin_eth0'].keys()) == {0}
@ -283,7 +284,7 @@ def test_cache_leadership():
#assert cache['ip_admin_eth0.netmask_admin_eth0'][0][0] is None
#assert cache['ip_admin_eth0.netmask_admin_eth0'][1][0] is None
cache = settings.get_cached()
assert set(cache.keys()) == set([None, 'ip_admin_eth0', 'ip_admin_eth0.ip_admin_eth0', 'ip_admin_eth0.netmask_admin_eth0'])
assert set(cache.keys()) == set(['ip_admin_eth0', 'ip_admin_eth0.ip_admin_eth0', 'ip_admin_eth0.netmask_admin_eth0'])
assert set(cache['ip_admin_eth0'].keys()) == set([None])
assert set(cache['ip_admin_eth0.ip_admin_eth0'].keys()) == set([None])
assert set(cache['ip_admin_eth0.netmask_admin_eth0'].keys()) == set([0, 1])
@ -309,19 +310,19 @@ def test_cache_callback():
od1 = OptionDescription('rootconfig', '', [val1, val2, val3, val4, val5])
cfg = Config(od1)
cfg.property.read_write()
cfg.value.dict()
cfg.value.get()
values = cfg._config_bag.context._impl_values_cache
settings = cfg._config_bag.context.properties_cache
compare(values.get_cached(), {'val1': {None: ('val', None)},
'val2': {None: ('val', None)},
'val3': {None: ('yes', None)},
'val4': {None: ('val', None)},
'val5': {None: (['yes'], None)}})
'val2': {None: ('val', None)},
'val3': {None: ('yes', None)},
'val4': {None: ('val', None)},
'val5': {None: (['yes'], None)}})
cfg.option('val1').value.set('new')
compare(values.get_cached(), {'val3': {None: ('yes', None)},
'val1': {None: ('new', None)},
'val5': {None: (['yes'], None)}})
cfg.value.dict()
cfg.value.get()
compare(values.get_cached(), {'val1': {None: ('new', None)},
'val2': {None: ('new', None)},
'val3': {None: ('yes', None)},
@ -334,7 +335,7 @@ def test_cache_callback():
'val1': {None: ('new', None)},
'val3': {None: ('new2', None, True)},
'val5': {None: (['yes'], None)}})
cfg.value.dict()
cfg.value.get()
compare(values.get_cached(), {'val1': {None: ('new', None)},
'val2': {None: ('new', None)},
'val3': {None: ('new2', None)},
@ -346,24 +347,12 @@ def test_cache_callback():
'val3': {None: ('new2', None)},
'val4': {None: ('new3', None, True)},
'val5': {None: (['yes'], None)}})
cfg.value.dict()
cfg.value.get()
compare(values.get_cached(), {'val1': {None: ('new', None)},
'val2': {None: ('new', None)},
'val3': {None: ('new2', None)},
'val4': {None: ('new3', None)},
'val5': {None: (['yes'], None)}})
cfg.option('val5').value.set([undefined, 'new4'])
compare(values.get_cached(), {'val1': {None: ('new', None)},
'val2': {None: ('new', None)},
'val3': {None: ('new2', None)},
'val4': {None: ('new3', None)},
'val5': {None: (['yes', 'new4'], None)}})
cfg.value.dict()
compare(values.get_cached(), {'val1': {None: ('new', None)},
'val2': {None: ('new', None)},
'val3': {None: ('new2', None)},
'val4': {None: ('new3', None)},
'val5': {None: (['yes', 'new4'], None)}})
# assert not list_sessions()
@ -374,7 +363,7 @@ def test_cache_leader_and_followers():
od1 = OptionDescription('rootconfig', '', [interface1])
cfg = Config(od1)
cfg.property.read_write()
cfg.value.dict()
cfg.value.get()
global_props = ['cache', 'disabled', 'frozen', 'hidden', 'validator', 'warnings', 'force_store_value']
val1_props = []
val1_val1_props = ['empty', 'unique']
@ -387,35 +376,31 @@ def test_cache_leader_and_followers():
idx_val2 = None
values = cfg._config_bag.context._impl_values_cache
settings = cfg._config_bag.context.properties_cache
compare(settings.get_cached(), {None: {None: (global_props, None)},
'val1': {None: (val1_props, None)},
'val1.val1': {None: (val1_val1_props, None)},
})
compare(settings.get_cached(), {'val1': {None: (val1_props, None)},
'val1.val1': {None: (val1_val1_props, None)},
})
# len is 0 so don't get any value
compare(values.get_cached(), {'val1.val1': {None: ([], None)}})
#
cfg.option('val1.val1').value.set([undefined])
cfg.option('val1.val1').value.set([None])
val_val2_props = {idx_val2: (val1_val2_props, None), None: (set(), None)}
compare(settings.get_cached(), {None: {None: (set(global_props), None)},
'val1.val1': {None: (val1_val1_props, None)},
})
compare(settings.get_cached(), {'val1.val1': {None: ({'empty', 'unique'}, None, True)}})
compare(values.get_cached(), {'val1.val1': {None: ([None], None, True)}})
cfg.value.dict()
cfg.value.get()
#has value
idx_val2 = 0
val_val2 = None
val_val2_props = {idx_val2: (val1_val2_props, None)}
compare(settings.get_cached(), {None: {None: (global_props, None)},
'val1': {None: (val1_props, None)},
compare(settings.get_cached(), {'val1': {None: (val1_props, None)},
'val1.val1': {None: (val1_val1_props, None)},
'val1.val2': val_val2_props})
compare(values.get_cached(), {'val1.val1': {None: ([None], None)},
'val1.val2': {idx_val2: (val_val2, None)},
})
cfg.option('val1.val1').value.set([undefined, undefined])
cfg.value.dict()
cfg.option('val1.val1').value.set([None, None])
cfg.value.get()
cfg.option('val1.val2', 1).value.set('oui')
compare(settings.get_cached(), {None: {None: (set(['cache', 'disabled', 'frozen', 'hidden', 'validator', 'warnings', 'force_store_value']), None)}})
compare(settings.get_cached(), {})
compare(values.get_cached(), {'val1.val2': {1: ('oui', None, True)}})
val1_val2_props = {0: (frozenset([]), None), 1: (frozenset([]), None)}
# assert not list_sessions()
@ -428,7 +413,7 @@ def test_cache_leader_callback():
od1 = OptionDescription('rootconfig', '', [interface1])
cfg = Config(od1)
cfg.property.read_write()
cfg.value.dict()
cfg.value.get()
global_props = ['cache', 'disabled', 'frozen', 'hidden', 'validator', 'warnings', 'force_store_value']
val1_props = []
val1_val1_props = ['empty', 'unique']
@ -439,18 +424,15 @@ def test_cache_leader_callback():
val1_val2_props = frozenset(val1_val2_props)
values = cfg._config_bag.context._impl_values_cache
settings = cfg._config_bag.context.properties_cache
compare(settings.get_cached(), {None: {None: (global_props, None)},
'val1': {None: (val1_props, None)},
compare(settings.get_cached(), {'val1': {None: (val1_props, None)},
'val1.val1': {None: (val1_val1_props, None)},
})
compare(values.get_cached(), {'val1.val1': {None: ([], None)}})
cfg.option('val1.val1').value.set([undefined])
compare(settings.get_cached(), {None: {None: (set(global_props), None)},
'val1.val1': {None: (val1_val1_props, None)},
})
cfg.option('val1.val1').value.set([None])
compare(settings.get_cached(), {'val1.val1': {None: ({'unique', 'empty'}, None, True)}})
compare(values.get_cached(), {'val1.val1': {None: ([None], None, True)}})
cfg.value.dict()
cfg.value.get()
# assert not list_sessions()
@ -469,39 +451,34 @@ def test_cache_requires():
settings = cfg._config_bag.context.properties_cache
assert values.get_cached() == {}
assert cfg.option('ip_address_service').value.get() == None
compare(settings.get_cached(), {None: {None: (set(['cache', 'disabled', 'frozen', 'hidden', 'validator', 'warnings', 'force_store_value']), None)},
'activate_service': {None: (set([]), None)},
'ip_address_service': {None: (set([]), None)}})
compare(settings.get_cached(), {'activate_service': {None: (set([]), None)},
'ip_address_service': {None: (set([]), None)}})
compare(values.get_cached(), {'ip_address_service': {None: (None, None)},
'activate_service': {None: (True, None)}})
cfg.value.dict()
compare(settings.get_cached(), {None: {None: (set(['cache', 'disabled', 'frozen', 'hidden', 'validator', 'warnings', 'force_store_value']), None)},
'activate_service': {None: (set([]), None)},
'ip_address_service': {None: (set([]), None)}})
cfg.value.get()
compare(settings.get_cached(), {'activate_service': {None: (set([]), None)},
'ip_address_service': {None: (set([]), None)}})
compare(values.get_cached(), {'ip_address_service': {None: (None, None)},
'activate_service': {None: (True, None)}})
'activate_service': {None: (True, None)}})
cfg.option('ip_address_service').value.set('1.1.1.1')
compare(settings.get_cached(), {None: {None: (set(['cache', 'disabled', 'frozen', 'hidden', 'validator', 'warnings', 'force_store_value']), None)},
'activate_service': {None: (set([]), None)}})
compare(settings.get_cached(), {'activate_service': {None: (set([]), None)}})
compare(values.get_cached(), {'activate_service': {None: (True, None)}, 'ip_address_service': {None: ('1.1.1.1', None, True)}})
cfg.value.dict()
compare(settings.get_cached(), {None: {None: (set(['cache', 'disabled', 'frozen', 'hidden', 'validator', 'warnings', 'force_store_value']), None)},
'activate_service': {None: (set([]), None)},
'ip_address_service': {None: (set([]), None)}})
cfg.value.get()
compare(settings.get_cached(), {'activate_service': {None: (set([]), None)},
'ip_address_service': {None: (set([]), None)}})
compare(values.get_cached(), {'ip_address_service': {None: ('1.1.1.1', None)},
'activate_service': {None: (True, None)}})
cfg.option('activate_service').value.set(False)
compare(settings.get_cached(), {None: {None: (set(['cache', 'disabled', 'frozen', 'hidden', 'validator', 'warnings', 'force_store_value']), None)}})
compare(settings.get_cached(), {})
compare(values.get_cached(), {'activate_service': {None: (False, None)}})
cfg.value.dict()
compare(settings.get_cached(), {None: {None: (set(['cache', 'disabled', 'frozen', 'hidden', 'validator', 'warnings', 'force_store_value']), None)},
'activate_service': {None: (set([]), None)},
'ip_address_service': {None: (set(['disabled']), None)}})
cfg.value.get()
compare(settings.get_cached(), {'activate_service': {None: (set([]), None)},
'ip_address_service': {None: (set(['disabled']), None)}})
compare(values.get_cached(), {'activate_service': {None: (False, None)}})
# assert not list_sessions()
@ -522,22 +499,19 @@ def test_cache_global_properties():
settings = cfg._config_bag.context.properties_cache
assert values.get_cached() == {}
assert cfg.option('ip_address_service').value.get() == None
compare(settings.get_cached(), {None: {None: (set(['cache', 'disabled', 'frozen', 'hidden', 'validator', 'warnings', 'force_store_value']), None)},
'activate_service': {None: (set([]), None)},
'ip_address_service': {None: (set([]), None)}})
compare(settings.get_cached(), {'activate_service': {None: (set([]), None)},
'ip_address_service': {None: (set([]), None)}})
compare(values.get_cached(), {'ip_address_service': {None: (None, None)},
'activate_service': {None: (True, None)}})
cfg.property.remove('disabled')
assert cfg.option('ip_address_service').value.get() == None
compare(settings.get_cached(), {None: {None: (set(['cache', 'frozen', 'hidden', 'validator', 'warnings', 'force_store_value']), None)},
'activate_service': {None: (set([]), None)},
'ip_address_service': {None: (set([]), None)}})
compare(settings.get_cached(), {'activate_service': {None: (set([]), None)},
'ip_address_service': {None: (set([]), None)}})
cfg.property.add('test')
assert cfg.option('ip_address_service').value.get() == None
compare(settings.get_cached(), {None: {None: (set(['cache', 'frozen', 'hidden', 'validator', 'warnings', 'test', 'force_store_value']), None)},
'activate_service': {None: (set([]), None)},
'ip_address_service': {None: (set([]), None)}})
compare(settings.get_cached(), {'activate_service': {None: (set([]), None)},
'ip_address_service': {None: (set([]), None)}})
# assert not list_sessions()

View file

@ -1,10 +1,9 @@
# coding: utf-8
from py.test import raises
from .autopath import do_autopath
do_autopath()
from .config import config_type, get_config, value_list, global_owner
from .config import config_type, get_config, value_list, global_owner, parse_od_get
from pytest import raises
from tiramisu import ChoiceOption, StrOption, OptionDescription, Config, owners, Calculation, \
undefined, Params, ParamValue, ParamOption
from tiramisu.error import ConfigError
@ -73,6 +72,30 @@ def test_choiceoption_function(config_type):
assert cfg.option('choice').owner.isdefault()
#
assert value_list(cfg.option('choice').value.list()) == ('val1', 'val2')
assert isinstance(cfg.option('choice').value.list(uncalculated=True), Calculation)
# assert not list_sessions()
def test_choiceoption_subfunction(config_type):
choice = ChoiceOption('choice', '', values=(Calculation(return_val, Params(ParamValue('val1'))), Calculation(return_val, Params(ParamValue('val2')))))
od1 = OptionDescription('od', '', [choice])
cfg = Config(od1)
cfg.property.read_write()
cfg = get_config(cfg, config_type)
owner = global_owner(cfg, config_type)
assert cfg.option('choice').owner.isdefault()
#
cfg.option('choice').value.set('val1')
assert cfg.option('choice').owner.get() == owner
#
cfg.option('choice').value.reset()
assert cfg.option('choice').owner.isdefault()
#
with raises(ValueError):
cfg.option('choice').value.set('no')
assert cfg.option('choice').owner.isdefault()
#
assert value_list(cfg.option('choice').value.list()) == ('val1', 'val2')
# assert not list_sessions()
@ -262,3 +285,13 @@ def test_choiceoption_calc_not_list():
with raises(ConfigError):
cfg.option('choice').value.set(['val1'])
# assert not list_sessions()
def test_choiceoption_calc_default_value():
var1 = StrOption("var1", '', default="val1")
var2 = StrOption("var2", '', default="val2")
choice = ChoiceOption("choice", '', values=(Calculation(return_val, Params((ParamOption(var1)))), Calculation(return_val, Params((ParamOption(var2))))), default="val1")
od2 = OptionDescription("rougail", '', children=[var1, var2, choice])
od1 = OptionDescription("baseoption", "", children=[od2])
cfg = Config(od1)
assert parse_od_get(cfg.value.get()) == {'rougail.var1': 'val1', 'rougail.var2': 'val2', 'rougail.choice': 'val1'}

View file

@ -9,10 +9,14 @@ do_autopath()
from .config import config_type, get_config, value_list, global_owner
import pytest
from tiramisu import Config
from tiramisu import Config, Calculation, Params, ParamSelfInformation, calc_value
from tiramisu.i18n import _
from tiramisu import Config, IntOption, FloatOption, ChoiceOption, \
BoolOption, StrOption, SymLinkOption, OptionDescription, undefined
BoolOption, StrOption, SymLinkOption, OptionDescription, undefined, \
DomainnameOption, EmailOption, URLOption, RegexpOption, IPOption, \
PortOption, NetworkOption, NetmaskOption, BroadcastOption, UsernameOption, \
GroupnameOption, DateOption, FilenameOption, PasswordOption, MACOption, \
PermissionsOption
from tiramisu.error import ConflictError, ConfigError, PropertiesOptionError
@ -26,8 +30,7 @@ def make_description():
floatoption = FloatOption('float', 'Test float option', default=2.3)
stroption = StrOption('str', 'Test string option', default="abc", properties=('mandatory', ))
boolop = BoolOption('boolop', 'Test boolean option op', default=True, properties=('hidden',))
wantref_option = BoolOption('wantref', 'Test requires', default=False)
wantref_option.impl_set_information('info', 'default value')
wantref_option = BoolOption('wantref', 'Test requires', default=False, informations={'info': 'default value'})
wantframework_option = BoolOption('wantframework', 'Test requires',
default=False)
@ -130,6 +133,29 @@ def test_not_valid_properties():
# assert not list_sessions()
def test_information_load():
ChoiceOption('a', '', ('a', 'b'), informations={'info': 'value'})
BoolOption('a', '', informations={'info': 'value'})
IntOption('a', '', informations={'info': 'value'})
FloatOption('a', '', informations={'info': 'value'})
StrOption('a', '', informations={'info': 'value'})
RegexpOption('a', '', informations={'info': 'value'})
IPOption('a', '', informations={'info': 'value'})
PortOption('a', '', informations={'info': 'value'})
NetworkOption('a', '', informations={'info': 'value'})
NetmaskOption('a', '', informations={'info': 'value'})
BroadcastOption('a', '', informations={'info': 'value'})
DomainnameOption('a', '', informations={'info': 'value'})
EmailOption('a', '', informations={'info': 'value'})
URLOption('a', '', informations={'info': 'value'})
UsernameOption('a', '', informations={'info': 'value'})
GroupnameOption('a', '', informations={'info': 'value'})
DateOption('a', '', informations={'info': 'value'})
FilenameOption('a', '', informations={'info': 'value'})
PasswordOption('a', '', informations={'info': 'value'})
MACOption('a', '', informations={'info': 'value'})
PermissionsOption('a', '', informations={'info': 'value'})
def test_information_config():
od1 = make_description()
cfg = Config(od1)
@ -143,11 +169,11 @@ def test_information_config():
with pytest.raises(ValueError):
cfg.information.get('noinfo')
assert cfg.information.get('noinfo', 'default') == 'default'
cfg.information.reset('info')
cfg.information.remove('info')
with pytest.raises(ValueError):
cfg.information.get('info')
cfg.information.remove('info')
with pytest.raises(ValueError):
cfg.information.reset('noinfo')
cfg.information.remove('noinfo')
assert list(cfg.information.list()) == ['doc']
# assert not list_sessions()
@ -194,26 +220,23 @@ def test_information_option():
with pytest.raises(ValueError):
cfg.option('gc.name').information.get('noinfo')
assert cfg.option('gc.name').information.get('noinfo', 'default') == 'default'
cfg.option('gc.name').information.reset('info')
cfg.option('gc.name').information.remove('info')
with pytest.raises(ValueError):
cfg.option('gc.name').information.get('info')
with pytest.raises(ValueError):
cfg.option('gc.name').information.reset('noinfo')
cfg.option('gc.name').information.remove('noinfo')
assert list(cfg.option('gc.name').information.list()) == ['doc']
#
assert cfg.option('wantref').information.get('info') == 'default value'
cfg.option('wantref').information.set('info', 'default value')
assert cfg.option('wantref').information.get('info') == 'default value'
cfg.option('wantref').information.reset('info')
cfg.option('wantref').information.remove('info')
assert cfg.option('wantref').information.get('info') == 'default value'
# assert not list_sessions()
def test_information_option_2():
i1 = IntOption('test1', '')
i1.impl_set_information('info', 'value')
# it's a dict
assert set(i1.impl_list_information()) == {'info', 'doc'}
i1 = IntOption('test1', '', informations={'info': 'value'})
od1 = OptionDescription('test', '', [i1])
cfg = Config(od1)
# it's tuples
@ -221,6 +244,17 @@ def test_information_option_2():
# assert not list_sessions()
def test_information_option_symlink():
i1 = IntOption('test1', '', Calculation(calc_value, Params(ParamSelfInformation('info'))), informations={'info': 'value'})
i2 = SymLinkOption('test2', i1)
od1 = OptionDescription('test', '', [i2, i1])
cfg = Config(od1)
# it's tuples
assert set(cfg.option('test1').information.list()) == {'info', 'doc'}
assert set(cfg.option('test2').information.list()) == {'info', 'doc'}
# assert not list_sessions()
def test_information_optiondescription():
od1 = make_description()
cfg = Config(od1)
@ -234,11 +268,11 @@ def test_information_optiondescription():
with pytest.raises(ValueError):
cfg.option('gc').information.get('noinfo')
assert cfg.option('gc').information.get('noinfo', 'default') == 'default'
cfg.option('gc').information.reset('info')
cfg.option('gc').information.remove('info')
with pytest.raises(ValueError):
cfg.option('gc').information.get('info')
with pytest.raises(ValueError):
cfg.option('gc').information.reset('noinfo')
cfg.option('gc').information.remove('noinfo')
assert list(cfg.option('gc').information.list()) == ['doc']
# assert not list_sessions()
@ -264,12 +298,12 @@ def test_get_modified_values():
compare(cfg.value.exportation(), {'od.g5': {None: ['yes', 'user']}})
cfg.option('od.g4').value.set(False)
compare(cfg.value.exportation(), {'od.g5': {None: ['yes', 'user']}, 'od.g4': {None: [False, 'user']}})
cfg.option('od.g4').value.set(undefined)
cfg.option('od.g4').value.set(True)
compare(cfg.value.exportation(), {'od.g5': {None: ['yes', 'user']}, 'od.g4': {None: [True, 'user']}})
cfg.option('od.g4').value.reset()
compare(cfg.value.exportation(), {'od.g5': {None: ['yes', 'user']}})
assert cfg.option('od.g6').ismulti()
cfg.option('od.g6').value.set([undefined])
cfg.option('od.g6').value.set([None])
compare(cfg.value.exportation(), {'od.g5': {None: ['yes', 'user']}, 'od.g6': {None: [[None], 'user']}})
cfg.option('od.g6').value.set([])
compare(cfg.value.exportation(), {'od.g5': {None: ['yes', 'user']}, 'od.g6': {None: [[], 'user']}})
@ -329,10 +363,10 @@ def test_config_multi(config_type):
cfg = get_config(cfg, config_type)
assert cfg.option('test1').value.get() == []
assert cfg.option('test2').value.get() == []
cfg.option('test2').value.set([undefined])
cfg.option('test2').value.set([1])
assert cfg.option('test2').value.get() == [1]
assert cfg.option('test3').value.get() == [2]
cfg.option('test3').value.set([undefined, undefined])
cfg.option('test3').value.set([2, 1])
assert cfg.option('test3').value.get() == [2, 1]
# assert not list_sessions()
@ -412,7 +446,7 @@ def test_config_od_name(config_type):
cfg = get_config(cfg, config_type)
assert cfg.option('val.i').name() == 'i'
assert cfg.option('val.s').name() == 's'
assert cfg.option('val.s').type() == _('integer')
assert cfg.option('val.s').type() == 'integer'
assert cfg.option('val').type() == 'optiondescription'
# assert not list_sessions()
@ -424,7 +458,7 @@ def test_config_od_type(config_type):
cfg = Config(o2)
cfg = get_config(cfg, config_type)
assert cfg.option('val').type() == 'optiondescription'
assert cfg.option('val.i').type() == _('integer')
assert cfg.option('val.i').type() == 'integer'
# assert not list_sessions()

View file

@ -3,7 +3,7 @@ from pytest import raises
from .autopath import do_autopath
do_autopath()
from .config import config_type, get_config, value_list, global_owner
from .config import config_type, get_config, value_list, global_owner, parse_od_get
from tiramisu import Config, IntOption, FloatOption, StrOption, ChoiceOption, \
BoolOption, FilenameOption, SymLinkOption, IPOption, \
@ -76,14 +76,12 @@ def test_make_dict(config_type):
cfg.property.read_write()
cfg.permissive.add('hidden')
cfg = get_config(cfg, config_type)
d = cfg.value.dict()
assert d == {"s1.a": False, "int": 42}
assert parse_od_get(cfg.value.get()) == {"s1.a": False, "int": 42}
cfg.option('int').value.set(43)
cfg.option('s1.a').value.set(True)
d = cfg.value.dict()
assert d == {"s1.a": True, "int": 43}
assert parse_od_get(cfg.value.get()) == {"s1.a": True, "int": 43}
if config_type == 'tiramisu':
assert cfg.forcepermissive.value.dict() == {"s1.a": True, "s1.b": False, "int": 43}
assert parse_od_get(cfg.forcepermissive.value.get()) == {"s1.a": True, "s1.b": False, "int": 43}
# assert not list_sessions()
@ -98,22 +96,7 @@ def test_make_dict_sub(config_type):
cfg.property.read_write()
cfg.permissive.add('hidden')
cfg = get_config(cfg, config_type)
assert cfg.option('s1').value.dict() == {'s1.a': False}
def test_make_dict_not_value(config_type):
"serialization part of config to a dict"
od1 = OptionDescription("opt", "", [
OptionDescription("s1", "", [
BoolOption("a", "", default=False),
BoolOption("b", "", default=False, properties=('hidden',))]),
IntOption("int", "", default=42)])
cfg = Config(od1)
cfg.property.read_write()
cfg.permissive.add('hidden')
cfg = get_config(cfg, config_type)
with raises(ConfigError):
cfg.option('s1.a').value.dict()
assert parse_od_get(cfg.option('s1').value.get()) == {'s1.a': False}
def test_make_dict_with_disabled(config_type):
@ -128,10 +111,10 @@ def test_make_dict_with_disabled(config_type):
cfg = Config(od1)
cfg.property.read_only()
cfg = get_config(cfg, config_type)
assert cfg.value.dict() == {"s1.a": False, "int": 42}
assert parse_od_get(cfg.value.get()) == {"s1.a": False, "int": 42}
if config_type == 'tiramisu':
assert cfg.forcepermissive.value.dict() == {"s1.a": False, "int": 42}
assert cfg.unrestraint.value.dict() == {"int": 42, "s1.a": False, "s1.b": False, "s2.a": False, "s2.b": False}
assert parse_od_get(cfg.forcepermissive.value.get()) == {"s1.a": False, "int": 42}
assert parse_od_get(cfg.unrestraint.value.get()) == {"int": 42, "s1.a": False, "s1.b": False, "s2.a": False, "s2.b": False}
# assert not list_sessions()
@ -147,8 +130,7 @@ def test_make_dict_with_disabled_in_callback(config_type):
cfg = Config(od1)
cfg.property.read_only()
cfg = get_config(cfg, config_type)
d = cfg.value.dict()
assert d == {"s1.a": False, "int": 42}
assert parse_od_get(cfg.value.get()) == {"s1.a": False, "int": 42}
# assert not list_sessions()
@ -166,150 +148,150 @@ def test_make_dict_fullpath(config_type):
cfg = Config(od1)
cfg.property.read_only()
cfg = get_config(cfg, config_type)
assert cfg.value.dict() == {"opt.s1.a": False, "opt.int": 42, "introot": 42}
assert cfg.option('opt').value.dict() == {"opt.s1.a": False, "opt.int": 42}
assert parse_od_get(cfg.value.get()) == {"opt.s1.a": False, "opt.int": 42, "introot": 42}
assert parse_od_get(cfg.option('opt').value.get()) == {"opt.s1.a": False, "opt.int": 42}
# assert not list_sessions()
def test_find_in_config():
"finds option in config"
od1 = make_description()
cfg = Config(od1)
cfg.property.read_only()
cfg.permissive.add('hidden')
ret = list(cfg.option.find('dummy'))
assert len(ret) == 1
_is_same_opt(ret[0].get(), cfg.option('gc.dummy').get())
#
ret_find = cfg.option.find('dummy', first=True)
ret = ret_find.get()
_is_same_opt(ret, cfg.option('gc.dummy').get())
#
ret = list(cfg.option.find('float'))
assert len(ret) == 2
_is_same_opt(ret[0].get(), cfg.option('gc.float').get())
_is_same_opt(ret[1].get(), cfg.option('float').get())
#
ret = cfg.option.find('bool', first=True)
_is_same_opt(ret.get(), cfg.option('gc.gc2.bool').get())
ret = cfg.option.find('bool', value=True, first=True)
_is_same_opt(ret.get(), cfg.option('bool').get())
ret = cfg.option.find('dummy', first=True)
_is_same_opt(ret.get(), cfg.option('gc.dummy').get())
ret = cfg.option.find('float', first=True)
_is_same_opt(ret.get(), cfg.option('gc.float').get())
ret = list(cfg.option.find('prop'))
assert len(ret) == 1
_is_same_opt(ret[0].get(), cfg.option('gc.prop').get())
#
ret = list(cfg.option.find('prop', value=None))
assert len(ret) == 1
ret = list(cfg.option.find('prop'))
assert len(ret) == 1
_is_same_opt(ret[0].get(), cfg.option('gc.prop').get())
#
cfg.property.read_write()
with raises(AttributeError):
ret = cfg.option.find('prop')
assert ret.get()
ret = list(cfg.unrestraint.option.find(name='prop'))
assert len(ret) == 2
_is_same_opt(ret[0].get(), cfg.unrestraint.option('gc.gc2.prop').get())
_is_same_opt(ret[1].get(), cfg.forcepermissive.option('gc.prop').get())
#
ret = list(cfg.forcepermissive.option.find('prop'))
assert len(ret) == 1
_is_same_opt(ret[0].get(), cfg.forcepermissive.option('gc.prop').get())
#
ret = cfg.forcepermissive.option.find('prop', first=True)
_is_same_opt(ret.get(), cfg.forcepermissive.option('gc.prop').get())
# combinaison of filters
ret = list(cfg.unrestraint.option.find('prop', type=BoolOption))
assert len(ret) == 1
_is_same_opt(ret[0].get(), cfg.unrestraint.option('gc.gc2.prop').get())
ret = cfg.unrestraint.option.find('prop', type=BoolOption, first=True)
_is_same_opt(ret.get(), cfg.unrestraint.option('gc.gc2.prop').get())
#
ret = list(cfg.option.find('dummy', value=False))
assert len(ret) == 1
_is_same_opt(ret[0].get(), cfg.option('gc.dummy').get())
#
ret = cfg.option.find('dummy', value=False, first=True)
_is_same_opt(ret.get(), cfg.option('gc.dummy').get())
#subcfgig
ret = list(cfg.option('gc').find('dummy'))
assert len(ret) == 1
_is_same_opt(ret[0].get(), cfg.option('gc.dummy').get())
#
ret = list(cfg.option('gc').find('float'))
assert len(ret) == 1
_is_same_opt(ret[0].get(), cfg.option('gc.float').get())
#
ret = list(cfg.option('gc.gc2').find('bool'))
assert len(ret) == 1
_is_same_opt(ret[0].get(), cfg.option('gc.gc2.bool').get())
ret = cfg.option('gc').find('bool', value=False, first=True)
_is_same_opt(ret.get(), cfg.option('gc.gc2.bool').get())
#
with raises(AttributeError):
ret = cfg.option('gc').find('bool', value=True, first=True)
assert ret.get()
#
with raises(AttributeError):
ret = cfg.option('gc').find('wantref')
ret.get()
#
ret = list(cfg.unrestraint.option('gc').find('prop'))
assert len(ret) == 2
_is_same_opt(ret[0].get(), cfg.unrestraint.option('gc.gc2.prop').get())
_is_same_opt(ret[1].get(), cfg.forcepermissive.option('gc.prop').get())
#
cfg.property.read_only()
ret = list(cfg.option('gc').find('prop'))
assert len(ret) == 1
_is_same_opt(ret[0].get(), cfg.option('gc.prop').get())
# not OptionDescription
with raises(AttributeError):
cfg.option.find('gc', first=True)
with raises(AttributeError):
cfg.option.find('gc2', first=True)
# assert not list_sessions()
def test_find_multi():
b = BoolOption('bool', '', multi=True, properties=('notunique',))
od1 = OptionDescription('od', '', [b])
cfg = Config(od1)
#
with raises(AttributeError):
list(cfg.option.find('bool', value=True))
with raises(AttributeError):
list(cfg.option.find('bool', value=True, first=True))
cfg.option('bool').value.set([False])
with raises(AttributeError):
list(cfg.option.find('bool', value=True))
with raises(AttributeError):
list(cfg.option.find('bool', value=True, first=True))
cfg.option('bool').value.set([False, False])
with raises(AttributeError):
list(cfg.option.find('bool', value=True))
with raises(AttributeError):
list(cfg.option.find('bool', value=True, first=True))
cfg.option('bool').value.set([False, False, True])
ret = list(cfg.option.find('bool', value=True))
assert len(ret) == 1
_is_same_opt(ret[0].get(), b)
ret = cfg.option.find('bool', value=True, first=True)
_is_same_opt(ret.get(), b)
# assert not list_sessions()
def test_does_not_find_in_config():
od1 = make_description()
cfg = Config(od1)
with raises(AttributeError):
list(cfg.option.find('IDontExist'))
# assert not list_sessions()
#def test_find_in_config():
# "finds option in config"
# od1 = make_description()
# cfg = Config(od1)
# cfg.property.read_only()
# cfg.permissive.add('hidden')
# ret = list(cfg.option.find('dummy'))
# assert len(ret) == 1
# _is_same_opt(ret[0].get(), cfg.option('gc.dummy').get())
# #
# ret_find = cfg.option.find('dummy', first=True)
# ret = ret_find.get()
# _is_same_opt(ret, cfg.option('gc.dummy').get())
# #
# ret = list(cfg.option.find('float'))
# assert len(ret) == 2
# _is_same_opt(ret[0].get(), cfg.option('gc.float').get())
# _is_same_opt(ret[1].get(), cfg.option('float').get())
# #
# ret = cfg.option.find('bool', first=True)
# _is_same_opt(ret.get(), cfg.option('gc.gc2.bool').get())
# ret = cfg.option.find('bool', value=True, first=True)
# _is_same_opt(ret.get(), cfg.option('bool').get())
# ret = cfg.option.find('dummy', first=True)
# _is_same_opt(ret.get(), cfg.option('gc.dummy').get())
# ret = cfg.option.find('float', first=True)
# _is_same_opt(ret.get(), cfg.option('gc.float').get())
# ret = list(cfg.option.find('prop'))
# assert len(ret) == 1
# _is_same_opt(ret[0].get(), cfg.option('gc.prop').get())
# #
# ret = list(cfg.option.find('prop', value=None))
# assert len(ret) == 1
# ret = list(cfg.option.find('prop'))
# assert len(ret) == 1
# _is_same_opt(ret[0].get(), cfg.option('gc.prop').get())
# #
# cfg.property.read_write()
# with raises(AttributeError):
# ret = cfg.option.find('prop')
# assert ret.get()
# ret = list(cfg.unrestraint.option.find(name='prop'))
# assert len(ret) == 2
# _is_same_opt(ret[0].get(), cfg.unrestraint.option('gc.gc2.prop').get())
# _is_same_opt(ret[1].get(), cfg.forcepermissive.option('gc.prop').get())
# #
# ret = list(cfg.forcepermissive.option.find('prop'))
# assert len(ret) == 1
# _is_same_opt(ret[0].get(), cfg.forcepermissive.option('gc.prop').get())
# #
# ret = cfg.forcepermissive.option.find('prop', first=True)
# _is_same_opt(ret.get(), cfg.forcepermissive.option('gc.prop').get())
# # combinaison of filters
# ret = list(cfg.unrestraint.option.find('prop', type=BoolOption))
# assert len(ret) == 1
# _is_same_opt(ret[0].get(), cfg.unrestraint.option('gc.gc2.prop').get())
# ret = cfg.unrestraint.option.find('prop', type=BoolOption, first=True)
# _is_same_opt(ret.get(), cfg.unrestraint.option('gc.gc2.prop').get())
# #
# ret = list(cfg.option.find('dummy', value=False))
# assert len(ret) == 1
# _is_same_opt(ret[0].get(), cfg.option('gc.dummy').get())
# #
# ret = cfg.option.find('dummy', value=False, first=True)
# _is_same_opt(ret.get(), cfg.option('gc.dummy').get())
# #subcfgig
# ret = list(cfg.option('gc').find('dummy'))
# assert len(ret) == 1
# _is_same_opt(ret[0].get(), cfg.option('gc.dummy').get())
# #
# ret = list(cfg.option('gc').find('float'))
# assert len(ret) == 1
# _is_same_opt(ret[0].get(), cfg.option('gc.float').get())
# #
# ret = list(cfg.option('gc.gc2').find('bool'))
# assert len(ret) == 1
# _is_same_opt(ret[0].get(), cfg.option('gc.gc2.bool').get())
# ret = cfg.option('gc').find('bool', value=False, first=True)
# _is_same_opt(ret.get(), cfg.option('gc.gc2.bool').get())
# #
# with raises(AttributeError):
# ret = cfg.option('gc').find('bool', value=True, first=True)
# assert ret.get()
# #
# with raises(AttributeError):
# ret = cfg.option('gc').find('wantref')
# ret.get()
# #
# ret = list(cfg.unrestraint.option('gc').find('prop'))
# assert len(ret) == 2
# _is_same_opt(ret[0].get(), cfg.unrestraint.option('gc.gc2.prop').get())
# _is_same_opt(ret[1].get(), cfg.forcepermissive.option('gc.prop').get())
# #
# cfg.property.read_only()
# ret = list(cfg.option('gc').find('prop'))
# assert len(ret) == 1
# _is_same_opt(ret[0].get(), cfg.option('gc.prop').get())
# # not OptionDescription
# with raises(AttributeError):
# cfg.option.find('gc', first=True)
# with raises(AttributeError):
# cfg.option.find('gc2', first=True)
## assert not list_sessions()
#
#
#def test_find_multi():
# b = BoolOption('bool', '', multi=True, properties=('notunique',))
# od1 = OptionDescription('od', '', [b])
# cfg = Config(od1)
# #
# with raises(AttributeError):
# list(cfg.option.find('bool', value=True))
# with raises(AttributeError):
# list(cfg.option.find('bool', value=True, first=True))
# cfg.option('bool').value.set([False])
# with raises(AttributeError):
# list(cfg.option.find('bool', value=True))
# with raises(AttributeError):
# list(cfg.option.find('bool', value=True, first=True))
# cfg.option('bool').value.set([False, False])
# with raises(AttributeError):
# list(cfg.option.find('bool', value=True))
# with raises(AttributeError):
# list(cfg.option.find('bool', value=True, first=True))
# cfg.option('bool').value.set([False, False, True])
# ret = list(cfg.option.find('bool', value=True))
# assert len(ret) == 1
# _is_same_opt(ret[0].get(), b)
# ret = cfg.option.find('bool', value=True, first=True)
# _is_same_opt(ret.get(), b)
## assert not list_sessions()
#
#
#def test_does_not_find_in_config():
# od1 = make_description()
# cfg = Config(od1)
# with raises(AttributeError):
# list(cfg.option.find('IDontExist'))
## assert not list_sessions()
def test_filename(config_type):
@ -430,7 +412,6 @@ def test_help():
cfg = Config(od2)
cfg.help(_display=False)
cfg.config.help(_display=False)
cfg.option.help(_display=False)
cfg.option('o').help(_display=False)
cfg.option('o.s').help(_display=False)
# assert not list_sessions()
@ -448,7 +429,7 @@ def test_config_reset():
#
cfg.option('gc.gc2.bool').value.set(True)
cfg.option('boolop').property.add('test')
cfg.option('float').permissive.set(frozenset(['test']))
cfg.option('float').permissive.add('test')
cfg.option('wantref').information.set('info', 'info')
assert cfg.option('gc.gc2.bool').value.get()
assert cfg.option('boolop').property.get()

View file

@ -66,7 +66,7 @@ def test_deref_optiondescription():
def test_deref_option_cache():
b = BoolOption('b', '')
o = OptionDescription('od', '', [b])
o._build_cache()
o._build_cache(None)
w = weakref.ref(b)
del(b)
assert w() is not None
@ -77,7 +77,7 @@ def test_deref_option_cache():
def test_deref_optiondescription_cache():
b = BoolOption('b', '')
o = OptionDescription('od', '', [b])
o._build_cache()
o._build_cache(None)
w = weakref.ref(o)
del(b)
assert w() is not None
@ -201,7 +201,7 @@ def test_deref_symlink():
def test_deref_dyn():
a = StrOption('a', '', ['val1', 'val2'], multi=True)
b = StrOption('b', '')
dod = DynOptionDescription('dod', '', [b], suffixes=Calculation(funcname, Params((ParamOption(a),))))
dod = DynOptionDescription('dod', '', [b], identifiers=Calculation(funcname, Params((ParamOption(a),))))
o = OptionDescription('od', '', [dod, a])
cfg = Config(o)
w = weakref.ref(a)

View file

@ -80,15 +80,15 @@ def test_copy_force_store_value():
assert conf.value.exportation() == {'creole.general.wantref': {None: [True, 'user']}}
assert conf2.value.exportation() == {'creole.general.wantref': {None: [False, 'forced']}}
# assert not list_sessions()
def test_copy_force_store_value_metaconfig():
od1 = make_description()
meta = MetaConfig([], optiondescription=od1)
conf = meta.config.new()
assert meta.property.get() == conf.property.get()
assert meta.permissive.get() == conf.permissive.get()
conf.property.read_write()
assert conf.value.exportation() == {'creole.general.wantref': {None: [False, 'forced']}}
assert meta.value.exportation() == {}
# assert not list_sessions()
#
#
#def test_copy_force_store_value_metaconfig():
# od1 = make_description()
# meta = MetaConfig([], optiondescription=od1)
# conf = meta.config.new()
# assert meta.property.get() == conf.property.get()
# assert meta.permissive.get() == conf.permissive.get()
# conf.property.read_write()
# assert conf.value.exportation() == {'creole.general.wantref': {None: [False, 'forced']}}
# assert meta.value.exportation() == {}
## assert not list_sessions()

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
# coding: utf-8
#test_force_store_value coding: utf-8
"frozen and hidden values"
from .autopath import do_autopath
do_autopath()

View file

@ -1,7 +1,7 @@
# coding: utf-8
from .autopath import do_autopath
do_autopath()
from .config import config_type, get_config, value_list, global_owner
from .config import config_type, get_config, value_list, global_owner, parse_od_get
import pytest
from tiramisu.setting import groups, owners
@ -59,10 +59,10 @@ def test_base_config(config_type):
cfg = get_config(cfg, config_type)
assert cfg.option('creole.general.activer_proxy_client').value.get() is False
assert cfg.option('creole.general.nom_machine').value.get() == "eoleng"
if config_type != 'tiramisu-api':
ret = cfg.option.find('nom_machine', first=True)
assert ret.value.get() == "eoleng"
assert cfg.option('creole').value.dict() == {'creole.general.numero_etab': None, 'creole.general.nom_machine': 'eoleng', 'creole.general.nombre_interfaces': 1, 'creole.general.activer_proxy_client': False, 'creole.general.mode_conteneur_actif': False, 'creole.general.serveur_ntp': [], 'creole.general.time_zone': 'Paris', 'creole.interface1.ip_admin_eth0.ip_admin_eth0': None, 'creole.interface1.ip_admin_eth0.netmask_admin_eth0': None}
# if config_type != 'tiramisu-api':
# ret = cfg.option.find('nom_machine', first=True)
# assert ret.value.get() == "eoleng"
assert parse_od_get(cfg.option('creole').value.get()) == {'creole.general.numero_etab': None, 'creole.general.nom_machine': 'eoleng', 'creole.general.nombre_interfaces': 1, 'creole.general.activer_proxy_client': False, 'creole.general.mode_conteneur_actif': False, 'creole.general.serveur_ntp': [], 'creole.general.time_zone': 'Paris', 'creole.interface1.ip_admin_eth0.ip_admin_eth0': None, 'creole.interface1.ip_admin_eth0.netmask_admin_eth0': None}
if config_type == 'tiramisu-api':
cfg.send()
# assert not list_sessions()
@ -77,49 +77,40 @@ def test_get_group_type():
assert grp.group_type() == 'family'
assert isinstance(grp.group_type(), groups.GroupType)
# assert not list_sessions()
def test_iter_on_groups():
od1 = make_description()
cfg = Config(od1)
cfg.property.read_write()
result = cfg.option('creole').list('optiondescription',
group_type=groups.family,
)
group_names = [res.name() for res in result]
assert group_names == ['general', 'interface1']
for i in cfg.option('creole').list('optiondescription',
group_type=groups.family,
):
#test StopIteration
break
result = cfg.option('creole').list('option',
group_type=groups.family,
)
assert list(result) == []
result = cfg.option('creole.general').list('optiondescription',
group_type=groups.family,
)
assert list(result) == []
# assert not list_sessions()
#
#
#def test_iter_on_groups():
# od1 = make_description()
# cfg = Config(od1)
# cfg.property.read_write()
# result = cfg.option('creole').list('optiondescription',
# group_type=groups.family,
# )
# group_names = [res.name() for res in result]
# assert group_names == ['general', 'interface1']
# for i in cfg.option('creole').list('optiondescription',
# group_type=groups.family,
# ):
# #test StopIteration
# break
# result = cfg.option('creole').list('option',
# group_type=groups.family,
# )
# assert list(result) == []
# result = cfg.option('creole.general').list('optiondescription',
# group_type=groups.family,
# )
# assert list(result) == []
## assert not list_sessions()
def test_list_recursive():
od1 = make_description()
cfg = Config(od1)
cfg.property.read_write()
result = cfg.option('creole').list('all')
result = cfg.option('creole').list()
group_names = [res.name() for res in result]
assert group_names == ['general', 'interface1']
#
result = cfg.option.list(recursive=True)
group_names = [res.name() for res in result]
assert group_names == ['numero_etab', 'nom_machine', 'nombre_interfaces',
'activer_proxy_client', 'mode_conteneur_actif',
'serveur_ntp', 'time_zone', 'ip_admin_eth0',
'netmask_admin_eth0']
result = list(cfg.option.list(recursive=True, type='optiondescription'))
group_names = [res.name() for res in result]
assert group_names == ['creole', 'general', 'interface1', 'ip_admin_eth0']
# assert not list_sessions()
@ -147,8 +138,7 @@ def test_iter_group_on_groups_force_permissive():
cfg = Config(od1)
cfg.property.read_write()
cfg.permissive.add('hidden')
result = cfg.forcepermissive.option('creole').list(type='optiondescription',
group_type=groups.family)
result = cfg.forcepermissive.option('creole').list()
group_names = [res.name() for res in result]
assert group_names == ['general', 'interface1', 'new']
# assert not list_sessions()
@ -159,8 +149,7 @@ def test_iter_on_groups_props():
cfg = Config(od1)
cfg.property.read_write()
cfg.option('creole.interface1').property.add('disabled')
result = cfg.option('creole').list(type='optiondescription',
group_type=groups.family)
result = cfg.option('creole').list()
group_names = [res.name() for res in result]
assert group_names == ['general']
# assert not list_sessions()
@ -170,20 +159,11 @@ def test_iter_on_empty_group():
od1 = OptionDescription("name", "descr", [])
cfg = Config(od1)
cfg.property.read_write()
result = list(cfg.option.list(type='optiondescription'))
result = list(cfg.list())
assert result == []
# assert not list_sessions()
def test_iter_not_group():
od1 = OptionDescription("name", "descr", [])
cfg = Config(od1)
cfg.property.read_write()
with pytest.raises(AssertionError):
print(list(cfg.option.list(type='optiondescription', group_type='family')))
# assert not list_sessions()
def test_groups_with_leader():
ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=True)
@ -214,32 +194,33 @@ def test_groups_is_leader(config_type):
assert not cfg.option('leadership.netmask_admin_eth0').isleader()
assert cfg.option('leadership.netmask_admin_eth0').isfollower()
assert cfg.option('leadership.netmask_admin_eth0').path() == 'leadership.netmask_admin_eth0'
assert cfg.option('leadership.netmask_admin_eth0').defaultmulti() == 'value'
assert cfg.option('leadership.netmask_admin_eth0').value.defaultmulti() == 'value'
if config_type == 'tiramisu-api':
cfg.send()
# assert not list_sessions()
def test_leader_list(config_type):
ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", ['val1'], multi=True)
netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=True, default_multi='value')
interface1 = Leadership('leadership', '', [ip_admin_eth0, netmask_admin_eth0])
od1 = OptionDescription('od', '', [interface1])
cfg = Config(od1)
cfg = get_config(cfg, config_type)
ret = cfg.option.list('all')
ret = cfg.list()
assert len(ret) == 1
assert ret[0].name() == 'leadership'
#
ret = cfg.option('leadership').list('all')
ret = cfg.option('leadership').list()
assert len(ret) == 2
assert ret[0].name() == 'ip_admin_eth0'
assert ret[1].name() == 'netmask_admin_eth0'
assert ret[1].index() == 0
#
cfg.option('leadership.ip_admin_eth0').value.set(['a', 'b'])
cfg.option('leadership.netmask_admin_eth0', 0).value.set('c')
cfg.option('leadership.netmask_admin_eth0', 1).value.set('d')
ret = cfg.option('leadership').list('all')
ret = cfg.option('leadership').list()
assert ret[0].name() == 'ip_admin_eth0'
assert ret[1].name() == 'netmask_admin_eth0'
# assert ret[1].option.index() == 0
@ -303,7 +284,7 @@ def test_groups_with_leader_make_dict(config_type):
od1 = OptionDescription('root', '', [interface1])
cfg = Config(od1)
cfg = get_config(cfg, config_type)
assert cfg.value.dict() == {'ip_admin_eth0.ip_admin_eth0': []}
assert parse_od_get(cfg.value.get()) == {'ip_admin_eth0.ip_admin_eth0': []}
if config_type != 'tiramisu-api':
# FIXME useful? already in leadership
assert cfg.option('ip_admin_eth0.ip_admin_eth0').value.len() == 0
@ -313,7 +294,7 @@ def test_groups_with_leader_make_dict(config_type):
# FIXME
assert cfg.option('ip_admin_eth0.ip_admin_eth0').value.len() == 2
assert cfg.option('ip_admin_eth0.netmask_admin_eth0').value.len() == 2
assert cfg.value.dict() == {'ip_admin_eth0.ip_admin_eth0': [{'ip_admin_eth0.ip_admin_eth0': 'ip1', 'ip_admin_eth0.netmask_admin_eth0': None}, {'ip_admin_eth0.ip_admin_eth0': 'ip2', 'ip_admin_eth0.netmask_admin_eth0': None}]}
assert parse_od_get(cfg.value.get()) == {'ip_admin_eth0.ip_admin_eth0': [{'ip_admin_eth0.ip_admin_eth0': 'ip1', 'ip_admin_eth0.netmask_admin_eth0': None}, {'ip_admin_eth0.ip_admin_eth0': 'ip2', 'ip_admin_eth0.netmask_admin_eth0': None}]}
if config_type == 'tiramisu-api':
cfg.send()
# assert not list_sessions()
@ -336,7 +317,7 @@ def test_groups_with_leader_make_dict2(config_type):
od1 = OptionDescription('root', '', [interface1])
cfg = Config(od1)
cfg = get_config(cfg, config_type)
assert cfg.value.dict() == {'other.ip_admin_eth0': []}
assert parse_od_get(cfg.value.get()) == {'other.ip_admin_eth0': []}
if config_type != 'tiramisu-api':
# FIXME useful? already in leadership
assert cfg.option('other.ip_admin_eth0').value.len() == 0
@ -346,7 +327,7 @@ def test_groups_with_leader_make_dict2(config_type):
# FIXME
assert cfg.option('other.ip_admin_eth0').value.len() == 2
assert cfg.option('other.netmask_admin_eth0').value.len() == 2
assert cfg.value.dict() == {'other.ip_admin_eth0': [{'other.ip_admin_eth0': 'ip1', 'other.netmask_admin_eth0': None}, {'other.ip_admin_eth0': 'ip2', 'other.netmask_admin_eth0': None}]}
assert parse_od_get(cfg.value.get()) == {'other.ip_admin_eth0': [{'other.ip_admin_eth0': 'ip1', 'other.netmask_admin_eth0': None}, {'other.ip_admin_eth0': 'ip2', 'other.netmask_admin_eth0': None}]}
if config_type == 'tiramisu-api':
cfg.send()
# assert not list_sessions()
@ -411,7 +392,7 @@ def test_groups_with_leader_hidden_in_config():
cfg.option('ip_admin_eth0.ip_admin_eth0').value.get()
with pytest.raises(PropertiesOptionError):
cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.get()
assert cfg.value.dict() == {}
assert parse_od_get(cfg.value.get()) == {}
# assert not list_sessions()
@ -426,16 +407,13 @@ def test_groups_with_leader_hidden_in_config2():
assert cfg.forcepermissive.option('ip_admin_eth0.ip_admin_eth0').value.get() == []
cfg.forcepermissive.option('ip_admin_eth0.ip_admin_eth0').value.set(['192.168.1.1'])
assert cfg.forcepermissive.option('ip_admin_eth0.netmask_admin_eth0', 0).value.get() is None
with pytest.raises(PropertiesOptionError):
cfg.option('ip_admin_eth0.ip_admin_eth0').value.get()
assert cfg.option('ip_admin_eth0.ip_admin_eth0').value.get() == ['192.168.1.1']
with pytest.raises(PropertiesOptionError):
cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.get()
assert cfg.value.dict() == {'ip_admin_eth0.ip_admin_eth0': ['192.168.1.1']}
assert cfg.value.dict(leader_to_list=True) == {'ip_admin_eth0.ip_admin_eth0': [{'ip_admin_eth0.ip_admin_eth0': '192.168.1.1'}]}
# assert not list_sessions()
def test_groups_with_leader_hidden_in_config2():
def test_groups_with_leader_hidden_in_config3():
ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=True, properties=('hidden',))
interface1 = Leadership('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])
@ -1064,7 +1042,7 @@ def test_follower_force_store_value_reset():
# assert not list_sessions()
def test_follower_properties():
#def test_follower_properties():
ip_admin_eth0 = StrOption('ip_admin_eth0', "ip réseau autorisé", multi=True)
netmask_admin_eth0 = StrOption('netmask_admin_eth0', "masque du sous-réseau", multi=True, properties=('aproperty',))
interface1 = Leadership('ip_admin_eth0', '', [ip_admin_eth0, netmask_admin_eth0])

View file

@ -1,12 +1,13 @@
# coding: utf-8
from .autopath import do_autopath
do_autopath()
from .config import parse_od_get
# FIXME from .config import config_type, get_config
import pytest
from tiramisu import Config
from tiramisu import IntOption, StrOption, OptionDescription, DynOptionDescription, \
SymLinkOption, Leadership, undefined, Calculation, Params, \
from tiramisu import IntOption, StrOption, OptionDescription, DynOptionDescription, PasswordOption, UsernameOption, \
SymLinkOption, Leadership, Calculation, Params, \
ParamOption, ParamValue, ParamIndex, calc_value
from tiramisu.error import PropertiesOptionError, ConfigError
from tiramisu.setting import groups
@ -14,6 +15,8 @@ from tiramisu.setting import groups
#def teardown_function(function):
# assert list_sessions() == [], 'session list is not empty when leaving "{}"'.format(function.__name__)
def is_mandatory(variable):
return True
def make_description():
@ -48,18 +51,6 @@ def make_description2():
return descr
def make_description_sym():
stroption = StrOption('str', 'Test string option', default="abc",
properties=('mandatory', ))
stroption1 = StrOption('str1', 'Test string option',
properties=('mandatory', ))
stroption2 = SymLinkOption('unicode2', stroption1)
stroption3 = StrOption('str3', 'Test string option', multi=True,
properties=('mandatory', ))
descr = OptionDescription('tiram', '', [stroption, stroption1, stroption2, stroption3])
return descr
def make_description3():
stroption = StrOption('str', 'Test string option', default="abc",
properties=('mandatory', ))
@ -97,7 +88,7 @@ def test_mandatory_ro_dict():
cfg.property.read_only()
prop = []
try:
cfg.value.dict()
cfg.value.get()
except PropertiesOptionError as err:
prop = err.proptype
assert 'mandatory' in prop
@ -106,14 +97,14 @@ def test_mandatory_ro_dict():
cfg.option('unicode2').value.set('yes')
cfg.property.read_only()
try:
cfg.value.dict()
cfg.value.get()
except PropertiesOptionError as err:
prop = err.proptype
assert 'mandatory' in prop
cfg.property.read_write()
cfg.option('str3').value.set(['yes'])
cfg.property.read_only()
assert cfg.value.dict() == {'str': 'abc', 'str1': 'yes', 'str3': ['yes'], 'unicode2': 'yes'}
assert parse_od_get(cfg.value.get()) == {'str': 'abc', 'str1': 'yes', 'str3': ['yes'], 'unicode2': 'yes'}
# assert not list_sessions()
@ -412,7 +403,7 @@ def test_mandatory_leader():
with pytest.raises(PropertiesOptionError):
cfg.option('ip_admin_eth0.ip_admin_eth0').value.get()
with pytest.raises(PropertiesOptionError):
cfg.value.dict()
cfg.value.get()
# assert not list_sessions()
@ -429,7 +420,7 @@ def test_mandatory_leader_sub():
with pytest.raises(PropertiesOptionError):
cfg.option('o.ip_admin_eth0.ip_admin_eth0').value.get()
with pytest.raises(PropertiesOptionError):
cfg.value.dict()
cfg.value.get()
# assert not list_sessions()
@ -455,7 +446,7 @@ def test_mandatory_leader_empty():
cfg.property.read_write()
assert cfg.option('ip_admin_eth0.ip_admin_eth0').value.get() == []
#
cfg.option('ip_admin_eth0.ip_admin_eth0').value.set([undefined])
cfg.option('ip_admin_eth0.ip_admin_eth0').value.set([None])
assert cfg.option('ip_admin_eth0.ip_admin_eth0').value.get() == [None]
assert cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.get() == None
cfg.property.read_only()
@ -501,7 +492,7 @@ def test_mandatory_warnings_leader_empty():
od1 = OptionDescription('o', '', [interface1])
cfg = Config(od1)
cfg.property.read_write()
cfg.option('ip_admin_eth0.ip_admin_eth0').value.set([undefined])
cfg.option('ip_admin_eth0.ip_admin_eth0').value.set([None])
assert cfg.option('ip_admin_eth0.ip_admin_eth0').value.get() == [None]
assert cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.get() == None
compare(cfg.value.mandatory(), ['ip_admin_eth0.ip_admin_eth0'])
@ -527,7 +518,7 @@ def test_mandatory_follower():
cfg = Config(od1)
cfg.property.read_only()
assert cfg.option('ip_admin_eth0.ip_admin_eth0').value.get() == []
assert cfg.value.dict() == {'ip_admin_eth0.ip_admin_eth0': []}
assert parse_od_get(cfg.value.get()) == {'ip_admin_eth0.ip_admin_eth0': []}
#
cfg.property.read_write()
cfg.option('ip_admin_eth0.ip_admin_eth0').value.set(['ip'])
@ -548,7 +539,7 @@ def test_mandatory_follower():
cfg.property.read_only()
assert cfg.option('ip_admin_eth0.ip_admin_eth0').value.get() == ['ip']
assert cfg.option('ip_admin_eth0.netmask_admin_eth0', 0).value.get() == 'ip'
assert cfg.value.dict() == {'ip_admin_eth0.ip_admin_eth0': [{'ip_admin_eth0.ip_admin_eth0': 'ip', 'ip_admin_eth0.netmask_admin_eth0': 'ip'}]}
assert parse_od_get(cfg.value.get()) == {'ip_admin_eth0.ip_admin_eth0': [{'ip_admin_eth0.ip_admin_eth0': 'ip', 'ip_admin_eth0.netmask_admin_eth0': 'ip'}]}
# assert not list_sessions()
@ -570,7 +561,14 @@ def test_mandatory_warnings_follower():
def test_mandatory_warnings_symlink():
od1 = make_description_sym()
stroption = StrOption('str', 'Test string option', default="abc",
properties=('mandatory', ))
stroption1 = StrOption('str1', 'Test string option',
properties=('mandatory', ))
stroption2 = SymLinkOption('unicode2', stroption1)
stroption3 = StrOption('str3', 'Test string option', multi=True,
properties=('mandatory', ))
od1 = OptionDescription('tiram', '', [stroption, stroption1, stroption2, stroption3])
cfg = Config(od1)
cfg.option('str').value.set('')
cfg.property.read_write()
@ -683,32 +681,32 @@ def test_mandatory_od_disabled():
# assert not list_sessions()
def return_list(val=None, suffix=None):
def return_list(val=None, identifier=None):
if val:
return val
else:
return ['val1', 'val2']
def test_mandatory_dyndescription():
st = StrOption('st', '', properties=('mandatory',))
dod = DynOptionDescription('dod', '', [st], suffixes=Calculation(return_list))
od = OptionDescription('od', '', [dod])
od2 = OptionDescription('od', '', [od])
cfg = Config(od2)
cfg.property.read_only()
compare(cfg.value.mandatory(), ['od.dodval1.stval1', 'od.dodval2.stval2'])
def test_mandatory_dyndescription_context():
val1 = StrOption('val1', '', ['val1', 'val2'], multi=True)
st = StrOption('st', '', properties=('mandatory',))
dod = DynOptionDescription('dod', '', [st], suffixes=Calculation(return_list, Params(ParamOption(val1))))
od = OptionDescription('od', '', [dod, val1])
od2 = OptionDescription('od', '', [od])
cfg = Config(od2)
cfg.property.read_only()
compare(cfg.value.mandatory(), ['od.dodval1.stval1', 'od.dodval2.stval2'])
#def test_mandatory_dyndescription():
# st = StrOption('st', '', properties=('mandatory',))
# dod = DynOptionDescription('dod', '', [st], identifiers=Calculation(return_list))
# od = OptionDescription('od', '', [dod])
# od2 = OptionDescription('od', '', [od])
# cfg = Config(od2)
# cfg.property.read_only()
# compare(cfg.value.mandatory(), ['od.dodval1.st', 'od.dodval2.st'])
#
#
#def test_mandatory_dyndescription_context():
# val1 = StrOption('val1', '', ['val1', 'val2'], multi=True)
# st = StrOption('st', '', properties=('mandatory',))
# dod = DynOptionDescription('dod', '', [st], identifiers=Calculation(return_list, Params(ParamOption(val1))))
# od = OptionDescription('od', '', [dod, val1])
# od2 = OptionDescription('od', '', [od])
# cfg = Config(od2)
# cfg.property.read_only()
# compare(cfg.value.mandatory(), ['od.dodval1.st', 'od.dodval2.st'])
def test_mandatory_callback_leader_and_followers_leader():
@ -723,3 +721,12 @@ def test_mandatory_callback_leader_and_followers_leader():
# FIXME cfg = get_config(cfg, config_type)
compare(cfg.value.mandatory(), ['val1.val1'])
# assert not list_sessions()
def test_mandatory_and_disabled():
password = PasswordOption(name="password", doc="Password", properties=frozenset({"disabled"}))
username = UsernameOption(name="username", doc="Username", properties=frozenset({"normal", Calculation(is_mandatory, Params((ParamOption(password)))), "disabled"}))
od1 = OptionDescription('rootconfig', '', [username, password])
cfg = Config(od1)
cfg.property.read_write()
cfg.value.get()

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -15,76 +15,76 @@ def make_metaconfig():
return MetaConfig([], optiondescription=od2, name='metacfg1')
def test_multi_parents_path():
"""
metacfg1 (1) ---
| -- cfg1
metacfg2 (2) ---
"""
metacfg1 = make_metaconfig()
cfg1 = metacfg1.config.new(type='config', name="cfg1")
metacfg2 = MetaConfig([cfg1], name='metacfg2')
#
assert metacfg1.config.path() == 'metacfg1'
assert metacfg2.config.path() == 'metacfg2'
assert cfg1.config.path() == 'metacfg2.metacfg1.cfg1'
def test_multi_parents_path_same():
"""
--- metacfg2 (1) ---
metacfg1 --| | -- cfg1
--- metacfg3 (2) ---
"""
metacfg1 = make_metaconfig()
metacfg2 = metacfg1.config.new(type='metaconfig', name="metacfg2")
metacfg3 = metacfg1.config.new(type='metaconfig', name="metacfg3")
cfg1 = metacfg2.config.new(type='config', name="cfg1")
metacfg3.config.add(cfg1)
#
assert metacfg2.config.path() == 'metacfg1.metacfg2'
assert metacfg3.config.path() == 'metacfg1.metacfg3'
assert cfg1.config.path() == 'metacfg1.metacfg3.metacfg1.metacfg2.cfg1'
metacfg1.option('od1.i1').value.set(1)
metacfg3.option('od1.i1').value.set(2)
assert cfg1.option('od1.i1').value.get() == 1
orideep = cfg1.config.deepcopy(metaconfig_prefix="test_", name='test_cfg1')
deep = orideep
while True:
try:
children = list(deep.config.list())
except:
break
assert len(children) < 2
deep = children[0]
assert deep.config.path() == 'test_metacfg3.test_metacfg1.test_metacfg2.test_cfg1'
assert cfg1.option('od1.i1').value.get() == 1
def test_multi_parents_value():
metacfg1 = make_metaconfig()
cfg1 = metacfg1.config.new(type='config', name="cfg1")
metacfg2 = MetaConfig([cfg1], name='metacfg2')
#
assert cfg1.option('od1.i1').value.get() == None
assert cfg1.option('od1.i2').value.get() == 1
assert cfg1.option('od1.i3').value.get() == None
#
assert metacfg1.option('od1.i1').value.get() == None
assert metacfg1.option('od1.i2').value.get() == 1
assert metacfg1.option('od1.i3').value.get() == None
#
assert metacfg2.option('od1.i1').value.get() == None
assert metacfg2.option('od1.i2').value.get() == 1
assert metacfg2.option('od1.i3').value.get() == None
#
metacfg1.option('od1.i3').value.set(3)
assert metacfg1.option('od1.i3').value.get() == 3
assert cfg1.option('od1.i3').value.get() == 3
assert metacfg2.option('od1.i2').value.get() == 1
#
metacfg2.option('od1.i2').value.set(4)
assert metacfg2.option('od1.i2').value.get() == 4
assert metacfg1.option('od1.i2').value.get() == 1
assert cfg1.option('od1.i2').value.get() == 4
#def test_multi_parents_path():
# """
# metacfg1 (1) ---
# | -- cfg1
# metacfg2 (2) ---
# """
# metacfg1 = make_metaconfig()
# cfg1 = metacfg1.config.new(type='config', name="cfg1")
# metacfg2 = MetaConfig([cfg1], name='metacfg2')
# #
# assert metacfg1.config.path() == 'metacfg1'
# assert metacfg2.config.path() == 'metacfg2'
# assert cfg1.config.path() == 'metacfg2.metacfg1.cfg1'
#
#
#def test_multi_parents_path_same():
# """
# --- metacfg2 (1) ---
# metacfg1 --| | -- cfg1
# --- metacfg3 (2) ---
# """
# metacfg1 = make_metaconfig()
# metacfg2 = metacfg1.config.new(type='metaconfig', name="metacfg2")
# metacfg3 = metacfg1.config.new(type='metaconfig', name="metacfg3")
# cfg1 = metacfg2.config.new(type='config', name="cfg1")
# metacfg3.config.add(cfg1)
# #
# assert metacfg2.config.path() == 'metacfg1.metacfg2'
# assert metacfg3.config.path() == 'metacfg1.metacfg3'
# assert cfg1.config.path() == 'metacfg1.metacfg3.metacfg1.metacfg2.cfg1'
# metacfg1.option('od1.i1').value.set(1)
# metacfg3.option('od1.i1').value.set(2)
# assert cfg1.option('od1.i1').value.get() == 1
# orideep = cfg1.config.deepcopy(metaconfig_prefix="test_", name='test_cfg1')
# deep = orideep
# while True:
# try:
# children = list(deep.config.list())
# except:
# break
# assert len(children) < 2
# deep = children[0]
# assert deep.config.path() == 'test_metacfg3.test_metacfg1.test_metacfg2.test_cfg1'
# assert cfg1.option('od1.i1').value.get() == 1
#
#
#
#def test_multi_parents_value():
# metacfg1 = make_metaconfig()
# cfg1 = metacfg1.config.new(type='config', name="cfg1")
# metacfg2 = MetaConfig([cfg1], name='metacfg2')
# #
# assert cfg1.option('od1.i1').value.get() == None
# assert cfg1.option('od1.i2').value.get() == 1
# assert cfg1.option('od1.i3').value.get() == None
# #
# assert metacfg1.option('od1.i1').value.get() == None
# assert metacfg1.option('od1.i2').value.get() == 1
# assert metacfg1.option('od1.i3').value.get() == None
# #
# assert metacfg2.option('od1.i1').value.get() == None
# assert metacfg2.option('od1.i2').value.get() == 1
# assert metacfg2.option('od1.i3').value.get() == None
# #
# metacfg1.option('od1.i3').value.set(3)
# assert metacfg1.option('od1.i3').value.get() == 3
# assert cfg1.option('od1.i3').value.get() == 3
# assert metacfg2.option('od1.i2').value.get() == 1
# #
# metacfg2.option('od1.i2').value.set(4)
# assert metacfg2.option('od1.i2').value.get() == 4
# assert metacfg1.option('od1.i2').value.get() == 1
# assert cfg1.option('od1.i2').value.get() == 4

View file

@ -20,7 +20,7 @@ def a_func():
return None
def display_name(*args):
def display_name(*args, with_quote=False):
return 'display_name'
@ -34,20 +34,6 @@ def test_option_valid_name():
i = SymLinkOption("test1", i)
def test_option_get_information():
description = "it's ok"
string = 'some informations'
i = IntOption('test', description)
with pytest.raises(ValueError):
i.impl_get_information('noinfo')
i.impl_set_information('info', string)
assert i.impl_get_information('info') == string
with pytest.raises(ValueError):
i.impl_get_information('noinfo')
assert i.impl_get_information('noinfo', 'default') == 'default'
assert i.impl_get_information('doc') == description
def test_option_get_information_config():
description = "it's ok"
string = 'some informations'
@ -55,21 +41,16 @@ def test_option_get_information_config():
od = OptionDescription('od', '', [i])
cfg = Config(od)
with pytest.raises(ValueError):
i.impl_get_information('noinfo')
with pytest.raises(AttributeError):
i.impl_set_information('info', string)
with pytest.raises(ValueError):
i.impl_get_information('noinfo')
assert i.impl_get_information('noinfo', 'default') == 'default'
assert i.impl_get_information('doc') == description
cfg.option('test').information.get('noinfo')
assert cfg.option('test').information.get('noinfo', 'default') == 'default'
assert cfg.option('test').information.get('doc') == description
# assert not list_sessions()
def test_option_unknown():
description = "it's ok"
string = 'some informations'
i = IntOption('test', description)
i.impl_set_information('noinfo', 'optdefault')
i = IntOption('test', description, informations={'noinfo': 'optdefault'})
od = OptionDescription('od', '', [i])
cfg = Config(od)
#
@ -93,8 +74,7 @@ def test_option_description():
def test_option_get_information_default():
description = "it's ok"
string = 'some informations'
i = IntOption('test', description)
i.impl_set_information('noinfo', 'optdefault')
i = IntOption('test', description, informations={'noinfo': 'optdefault'})
od = OptionDescription('od', '', [i])
cfg = Config(od)
#
@ -108,32 +88,30 @@ def test_option_get_information_default():
def test_option_get_information_config2():
description = "it's ok"
string = 'some informations'
i = IntOption('test', description)
i.impl_set_information('info', string)
i = IntOption('test', description, informations={'info': string})
od = OptionDescription('od', '', [i])
cfg = Config(od)
with pytest.raises(ValueError):
i.impl_get_information('noinfo')
with pytest.raises(AttributeError):
i.impl_set_information('info', 'hello')
assert i.impl_get_information('info') == string
cfg.option('test').information.get('noinfo')
assert cfg.option('test').information.get('info') == string
with pytest.raises(ValueError):
i.impl_get_information('noinfo')
assert i.impl_get_information('noinfo', 'default') == 'default'
assert i.impl_get_information('doc') == description
cfg.option('test').information.get('noinfo')
assert cfg.option('test').information.get('noinfo', 'default') == 'default'
assert cfg.option('test').information.get('doc') == description
# assert not list_sessions()
def test_optiondescription_get_information():
description = "it's ok"
string = 'some informations'
o = OptionDescription('test', description, [])
o.impl_set_information('info', string)
assert o.impl_get_information('info') == string
o = OptionDescription('test', description, [], informations={'info': string})
od = OptionDescription('od', '', [o])
cfg = Config(od)
assert cfg.option('test').information.get('info') == string
with pytest.raises(ValueError):
o.impl_get_information('noinfo')
assert o.impl_get_information('noinfo', 'default') == 'default'
assert o.impl_get_information('doc') == description
cfg.option('test').information.get('noinfo')
assert cfg.option('test').information.get('noinfo', 'default') == 'default'
assert cfg.option('test').information.get('doc') == description
# assert not list_sessions()
@ -202,24 +180,9 @@ def test_optiondescription_list():
od2 = OptionDescription('od', '', [od1, od3])
od4 = OptionDescription('od', '', [od2])
cfg = Config(od4)
assert len(list(cfg.option('od').list('option'))) == 0
assert len(list(cfg.option('od').list('optiondescription'))) == 2
assert len(list(cfg.option('od').list('optiondescription', group_type=groups.family))) == 1
assert len(list(cfg.option('od').list('optiondescription', group_type=groups.notfamily1))) == 1
assert len(list(cfg.option('od.od').list('option'))) == 1
assert len(list(cfg.option('od.od2').list('option'))) == 1
try:
list(cfg.option('od').list('unknown'))
except AssertionError:
pass
else:
raise Exception('must raise')
try:
list(cfg.option('od').list('option', group_type='toto'))
except AssertionError:
pass
else:
raise Exception('must raise')
assert len(list(cfg.option('od').list())) == 2
assert len(list(cfg.option('od.od').list())) == 1
assert len(list(cfg.option('od.od2').list())) == 1
# assert not list_sessions()
@ -233,22 +196,7 @@ def test_optiondescription_group():
od3.impl_set_group_type(groups.notfamily)
od2 = OptionDescription('od', '', [od1, od3])
cfg = Config(od2)
assert len(list(cfg.option.list('option'))) == 0
assert len(list(cfg.option.list('optiondescription'))) == 2
assert len(list(cfg.option.list('optiondescription', group_type=groups.family))) == 1
assert len(list(cfg.option.list('optiondescription', group_type=groups.notfamily))) == 1
try:
list(cfg.option.list('unknown'))
except AssertionError:
pass
else:
raise Exception('must raise')
try:
list(cfg.option.list('option', group_type='toto'))
except AssertionError:
pass
else:
raise Exception('must raise')
assert len(list(cfg.list())) == 2
# assert not list_sessions()
@ -351,4 +299,4 @@ def test_option_display_name():
display_name=display_name,
)
assert cfg.option('test1').name() == 'test1'
assert cfg.option('test1').doc() == 'display_name'
assert cfg.option('test1').description() == 'display_name'

View file

@ -13,7 +13,7 @@ from tiramisu import ChoiceOption, BoolOption, IntOption, FloatOption, \
valid_ip_netmask, ParamSelfOption, ParamInformation, ParamSelfInformation
from tiramisu.error import PropertiesOptionError, ConflictError, LeadershipError, ConfigError
from tiramisu.i18n import _
from .config import config_type, get_config
from .config import config_type, get_config, parse_od_get
def return_val():
@ -289,16 +289,34 @@ def test_callback(config_type):
cfg = Config(od1)
cfg.property.read_write()
cfg = get_config(cfg, config_type)
assert isinstance(cfg.option('val1').value.get(uncalculated=True), Calculation)
assert cfg.option('val1').value.get() == 'val'
cfg.option('val1').value.set('new-val')
assert cfg.option('val1').value.get() == 'new-val'
with pytest.raises(ConfigError):
assert cfg.option('val1').defaultmulti() == None
assert cfg.option('val1').value.defaultmulti() == None
cfg.option('val1').value.reset()
assert cfg.option('val1').value.get() == 'val'
# assert not list_sessions()
def test_callback_set(config_type):
val1 = StrOption('val1', "")
val2 = StrOption('val2', "")
od1 = OptionDescription('rootconfig', '', [val1, val2])
cfg = Config(od1)
cfg.property.read_write()
cfg = get_config(cfg, config_type)
cfg.option('val2').value.set(Calculation(return_value, Params(ParamOption(val1))))
assert cfg.option('val2').value.get() == None
#
cfg.option('val1').value.set('new-val')
assert cfg.option('val2').value.get() == 'new-val'
#
cfg.option('val1').value.reset()
assert cfg.option('val2').value.get() == None
def test_params():
with pytest.raises(ValueError):
Params('str')
@ -398,8 +416,7 @@ def test_callback_information(config_type):
def test_callback_information2(config_type):
val1 = StrOption('val1', "", Calculation(return_value, Params(ParamSelfInformation('information', 'no_value'))))
val2 = StrOption('val2', "", Calculation(return_value, Params(ParamSelfInformation('information'))))
val2.impl_set_information('information', 'new_value')
val2 = StrOption('val2', "", Calculation(return_value, Params(ParamSelfInformation('information'))), informations={'information': 'new_value'})
val3 = StrOption('val3', "", Calculation(return_value, Params(ParamSelfInformation('information'))))
od1 = OptionDescription('rootconfig', '', [val1, val2, val3])
cfg = Config(od1)
@ -415,9 +432,8 @@ def test_callback_information2(config_type):
def test_callback_information3(config_type):
val1 = StrOption('val1', "")
val1 = StrOption('val1', "", informations={'information': 'new_value'})
val2 = StrOption('val2', "", Calculation(return_value, Params(ParamInformation('information', option=val1))))
val1.impl_set_information('information', 'new_value')
od1 = OptionDescription('rootconfig', '', [val1, val2])
cfg = Config(od1)
cfg.property.read_write()
@ -532,7 +548,7 @@ def test_callback_multi(config_type):
cfg = get_config(cfg, config_type)
assert cfg.option('val1').value.get() == ['val']
cfg.option('val1').value.set(['new-val'])
assert cfg.option('val1').defaultmulti() == None
assert cfg.option('val1').value.defaultmulti() == None
assert cfg.option('val1').value.get() == ['new-val']
cfg.option('val1').value.set(['new-val', 'new-val2'])
assert cfg.option('val1').value.get() == ['new-val', 'new-val2']
@ -541,6 +557,21 @@ def test_callback_multi(config_type):
# assert not list_sessions()
def test_callback_multi_set(config_type):
val1 = StrOption('val1', "", multi=True)
od1 = OptionDescription('rootconfig', '', [val1])
cfg = Config(od1)
cfg.property.read_write()
cfg = get_config(cfg, config_type)
assert cfg.option('val1').value.get() == []
#
cfg.option('val1').value.set([Calculation(return_val)])
assert cfg.option('val1').value.get() == ['val']
cfg.option('val1').value.reset()
assert cfg.option('val1').value.get() == []
# assert not list_sessions()
def test_callback_multi_value(config_type):
val1 = StrOption('val1', "", ['val'], multi=True)
option = ParamOption(val1)
@ -577,6 +608,30 @@ def test_callback_multi_value(config_type):
# assert not list_sessions()
def test_callback_multi_value_set(config_type):
val1 = StrOption('val1', "", ['val1'], multi=True)
val2 = StrOption('val2', "", ['val2'], multi=True)
od1 = OptionDescription('rootconfig', '', [val1, val2])
cfg = Config(od1)
cfg.property.read_write()
cfg = get_config(cfg, config_type)
assert cfg.option('val1').value.get() == ['val1']
assert cfg.option('val2').value.get() == ['val2']
#
cfg.option('val2').value.set(Calculation(return_value, Params(ParamOption(val1))))
assert cfg.option('val1').value.get() == ['val1']
assert cfg.option('val2').value.get() == ['val1']
#
cfg.option('val1').value.set(['val1', 'yes'])
assert cfg.option('val2').value.get() == ['val1', 'yes']
assert cfg.option('val2').value.get() == ['val1', 'yes']
#
cfg.option('val2').value.reset()
assert cfg.option('val1').value.get() == ['val1', 'yes']
assert cfg.option('val2').value.get() == ['val2']
# assert not list_sessions()
def test_callback_multi_list(config_type):
val1 = StrOption('val1', "", Calculation(return_list), multi=True, properties=('notunique',))
od1 = OptionDescription('rootconfig', '', [val1])
@ -611,7 +666,7 @@ def test_callback_multi_callback(config_type):
cfg.property.read_write()
cfg = get_config(cfg, config_type)
assert cfg.option('val1.val1').value.get() == ['val']
cfg.option('val1.val1').value.set(['val1', undefined])
cfg.option('val1.val1').value.set(['val1', None])
assert cfg.option('val1.val1').value.get() == ['val1', None]
# assert not list_sessions()
@ -624,27 +679,53 @@ def test_callback_multi_callback_default(config_type):
cfg.property.read_write()
cfg = get_config(cfg, config_type)
assert cfg.option('val1.val1').value.get() == []
cfg.option('val1.val1').value.set(['val1', undefined])
cfg.option('val1.val1').value.set(['val1', 'val'])
assert cfg.option('val1.val1').value.get() == ['val1', 'val']
# assert not list_sessions()
def test_callback_leader_and_followers_leader(config_type):
val1 = StrOption('val1', "", default=[Calculation(return_val)], default_multi=Calculation(return_val), multi=True, properties=('notunique',))
val2 = StrOption('val2', "", multi=True)
interface1 = Leadership('val1', '', [val1, val2])
od1 = OptionDescription('rootconfig', '', [interface1])
val1 = StrOption('val1', "", default=['val'], multi=True)
val2 = StrOption('val2', "", default=Calculation(return_value, Params(ParamOption(val1))), default_multi=Calculation(return_val), multi=True, properties=('notunique',))
val3 = StrOption('val3', "", multi=True)
interface1 = Leadership('val2', '', [val2, val3])
od1 = OptionDescription('rootconfig', '', [val1, interface1])
cfg = Config(od1)
cfg.property.read_write()
cfg = get_config(cfg, config_type)
assert cfg.option('val1.val1').value.get() == ['val']
cfg.option('val1.val1').value.set([undefined, undefined])
assert cfg.option('val1.val1').value.get() == ['val', 'val']
assert cfg.option('val1.val2', 0).value.get() == None
assert cfg.option('val1.val2', 1).value.get() == None
assert cfg.option('val1').value.get() == ['val']
assert cfg.option('val2.val2').value.get() == ['val']
#
cfg.option('val1').value.set(['val1', 'val2'])
assert cfg.option('val1').value.get() == ['val1', 'val2']
assert cfg.option('val2.val2').value.get() == ['val1', 'val2']
assert cfg.option('val2.val3', 0).value.get() == None
assert cfg.option('val2.val3', 1).value.get() == None
#
cfg.option('val1').value.set(['val'])
assert cfg.option('val2.val2').value.get() == ['val']
assert cfg.option('val2.val3', 0).value.get() == None
# assert not list_sessions()
def test_callback_leader_and_followers_leader_set(config_type):
val1 = StrOption('val1', "", default=['val1', 'val2'], multi=True)
val2 = StrOption('val2', "", multi=True)
val3 = StrOption('val3', "", multi=True)
interface1 = Leadership('val2', '', [val2, val3])
od1 = OptionDescription('rootconfig', '', [val1, interface1])
cfg = Config(od1)
cfg.property.read_write()
cfg = get_config(cfg, config_type)
assert cfg.option('val1').value.get() == ['val1', 'val2']
#
cfg.option('val2.val2').value.set(Calculation(return_value, Params(ParamOption(val1))))
assert cfg.option('val2.val2').value.get() == ['val1', 'val2']
assert cfg.option('val2.val3', 0).value.get() == None
assert cfg.option('val2.val3', 1).value.get() == None
#assert not list_sessions()
def test_callback_follower(config_type):
val1 = StrOption('val1', "", multi=True)
val2 = StrOption('val2', "", Calculation(return_value3, Params(ParamValue(['string', 'new']), {'index': ParamIndex()})), multi=True)
@ -670,6 +751,28 @@ def test_callback_follower(config_type):
# assert not list_sessions()
def test_callback_follower_set(config_type):
val1 = StrOption('val1', "")
val2 = StrOption('val2', "", default=['val1'], multi=True)
val3 = StrOption('val3', "", multi=True)
interface1 = Leadership('val2', '', [val2, val3])
od1 = OptionDescription('rootconfig', '', [val1, interface1])
cfg = Config(od1)
cfg.property.read_write()
cfg = get_config(cfg, config_type)
cfg.option('val1').value.set('val')
assert cfg.option('val1').value.get() == 'val'
assert cfg.option('val2.val2').value.get() == ['val1']
assert cfg.option('val2.val3', 0).value.get() == None
#
cfg.option('val2.val3', 0).value.set(Calculation(return_value, Params(ParamOption(val1))))
assert cfg.option('val2.val3', 0).value.get() == 'val'
#
cfg.option('val1').value.set('val1')
assert cfg.option('val2.val3', 0).value.get() == 'val1'
# assert not list_sessions()
def test_callback_leader_and_followers_leader2(config_type):
val1 = StrOption('val1', "", multi=True)
val2 = StrOption('val2', "", multi=True, default_multi='val2')
@ -704,7 +807,7 @@ def test_callback_leader_and_followers_leader_mandatory1(config_type):
cfg.send()
cfg_ori.property.read_write()
cfg = get_config(cfg_ori, config_type)
cfg.option('val1.val1').value.set([undefined, 'val3'])
cfg.option('val1.val1').value.set(['val', 'val3'])
if config_type == 'tiramisu-api':
cfg.send()
cfg_ori.property.read_only()
@ -842,7 +945,7 @@ def test_consistency_leader_and_followers_leader_mandatory_transitive():
try:
cfg.option('val1.val2', 0).value.get()
except PropertiesOptionError as error:
assert str(error) == str(_('cannot access to {0} "{1}" because has {2} {3}').format('option', 'val2', _('property'), '"disabled"'))
assert str(error) == str(_('cannot access to {0} {1} because has {2} {3}').format('option', '"val2"', _('property'), '"disabled"'))
else:
raise Exception('must raises')
assert list(cfg.value.mandatory()) == []
@ -860,7 +963,8 @@ def test_callback_leader_and_followers_leader_list(config_type):
assert cfg.option('val1.val1').value.get() == ['val', 'val']
assert cfg.option('val1.val2', 0).value.get() == None
assert cfg.option('val1.val2', 1).value.get() == None
cfg.option('val1.val1').value.set(['val', 'val', undefined])
default_multi = cfg.option('val1.val1').value.defaultmulti()
cfg.option('val1.val1').value.set(['val', 'val', default_multi])
assert cfg.option('val1.val1').value.get() == ['val', 'val', None]
assert cfg.option('val1.val2', 0).value.get() == None
assert cfg.option('val1.val2', 1).value.get() == None
@ -1398,6 +1502,22 @@ def test_leadership_callback_description(config_type):
# assert not list_sessions()
def test_leadership_callback_outside(config_type):
st1 = StrOption('st1', "", multi=True)
st2 = StrOption('st2', "", multi=True, default_multi='val2')
stm = Leadership('st1', '', [st1, st2])
st3 = StrOption('st3', "", Calculation(return_value, Params(ParamOption(st2))), multi=True)
st = OptionDescription('st', '', [stm, st3])
od = OptionDescription('od', '', [st])
od2 = OptionDescription('od', '', [od])
cfg = Config(od2)
cfg = get_config(cfg, config_type)
owner = cfg.owner.get()
cfg.option('od.st.st1.st1').value.set(['yes'])
assert parse_od_get(cfg.value.get()) == {'od.st.st1.st1': [{'od.st.st1.st1': 'yes', 'od.st.st1.st2': 'val2'}], 'od.st.st3': ['val2']}
## assert not list_sessions()
def test_callback_raise():
opt1 = BoolOption('opt1', 'Option 1', Calculation(return_raise))
opt2 = BoolOption('opt2', 'Option 2', Calculation(return_valueerror))
@ -1423,7 +1543,7 @@ def test_calc_value_simple(config_type):
od1 = OptionDescription('root', '', [val1, val2])
cfg = Config(od1)
cfg = get_config(cfg, config_type)
assert cfg.value.dict() == {'val1': 'val1', 'val2': 'val1'}
assert parse_od_get(cfg.value.get()) == {'val1': 'val1', 'val2': 'val1'}
# assert not list_sessions()
@ -1434,7 +1554,7 @@ def test_calc_value_multi(config_type):
od1 = OptionDescription('root', '', [val1, val2, val3])
cfg = Config(od1)
cfg = get_config(cfg, config_type)
assert cfg.value.dict() == {'val1': 'val1', 'val2': 'val2', 'val3': ['val1', 'val2']}
assert parse_od_get(cfg.value.get()) == {'val1': 'val1', 'val2': 'val2', 'val3': ['val1', 'val2']}
# assert not list_sessions()
@ -1444,9 +1564,9 @@ def test_calc_value_disabled():
od1 = OptionDescription('root', '', [val1, val2])
cfg = Config(od1)
cfg.property.read_write()
assert cfg.value.dict() == {'val1': 'val1', 'val2': 'val1'}
assert parse_od_get(cfg.value.get()) == {'val1': 'val1', 'val2': 'val1'}
cfg.option('val1').property.add('disabled')
assert cfg.value.dict() == {'val2': 'default_value'}
assert parse_od_get(cfg.value.get()) == {'val2': 'default_value'}
# assert not list_sessions()
@ -1461,9 +1581,9 @@ def test_calc_value_condition(config_type):
cfg = Config(od1)
cfg.property.read_write()
cfg = get_config(cfg, config_type)
assert cfg.value.dict() == {'boolean': True, 'val1': 'val1', 'val2': 'val1'}
assert parse_od_get(cfg.value.get()) == {'boolean': True, 'val1': 'val1', 'val2': 'val1'}
cfg.option('boolean').value.set(False)
assert cfg.value.dict() == {'boolean': False, 'val1': 'val1', 'val2': 'default_value'}
assert parse_od_get(cfg.value.get()) == {'boolean': False, 'val1': 'val1', 'val2': 'default_value'}
# assert not list_sessions()
@ -1474,7 +1594,7 @@ def test_calc_value_allow_none(config_type):
od1 = OptionDescription('root', '', [val1, val2, val3])
cfg = Config(od1)
cfg = get_config(cfg, config_type)
assert cfg.value.dict() == {'val1': 'val1', 'val2': None, 'val3': ['val1', None]}
assert parse_od_get(cfg.value.get()) == {'val1': 'val1', 'val2': None, 'val3': ['val1', None]}
# assert not list_sessions()
@ -1485,7 +1605,7 @@ def test_calc_value_remove_duplicate(config_type):
od1 = OptionDescription('root', '', [val1, val2, val3])
cfg = Config(od1)
cfg = get_config(cfg, config_type)
assert cfg.value.dict() == {'val1': 'val1', 'val2': 'val1', 'val3': ['val1']}
assert parse_od_get(cfg.value.get()) == {'val1': 'val1', 'val2': 'val1', 'val3': ['val1']}
# assert not list_sessions()
@ -1496,7 +1616,7 @@ def test_calc_value_remove_duplicate2(config_type):
od1 = OptionDescription('root', '', [val1, val2, val3])
cfg = Config(od1)
cfg = get_config(cfg, config_type)
assert cfg.value.dict() == {'val1': ['val1', 'val1'], 'val2': ['val1', 'val1'], 'val3': ['val1-val1']}
assert parse_od_get(cfg.value.get()) == {'val1': ['val1', 'val1'], 'val2': ['val1', 'val1'], 'val3': ['val1-val1']}
# assert not list_sessions()
@ -1508,9 +1628,9 @@ def test_calc_value_join(config_type):
od1 = OptionDescription('root', '', [val1, val2, val3, val4])
cfg = Config(od1)
cfg = get_config(cfg, config_type)
assert cfg.value.dict() == {'val1': 'val1', 'val2': 'val2', 'val3': None, 'val4': None}
assert parse_od_get(cfg.value.get()) == {'val1': 'val1', 'val2': 'val2', 'val3': None, 'val4': None}
cfg.option('val3').value.set('val3')
assert cfg.value.dict() == {'val1': 'val1', 'val2': 'val2', 'val3': 'val3', 'val4': 'val1.val2.val3'}
assert parse_od_get(cfg.value.get()) == {'val1': 'val1', 'val2': 'val2', 'val3': 'val3', 'val4': 'val1.val2.val3'}
# assert not list_sessions()
@ -1520,9 +1640,9 @@ def test_calc_value_join_multi(config_type):
od1 = OptionDescription('root', '', [val1, val4])
cfg = Config(od1)
cfg = get_config(cfg, config_type)
assert cfg.value.dict() == {'val1': [], 'val4': []}
assert parse_od_get(cfg.value.get()) == {'val1': [], 'val4': []}
cfg.option('val1').value.set(['val1'])
assert cfg.value.dict() == {'val1': ['val1'], 'val4': ['val1']}
assert parse_od_get(cfg.value.get()) == {'val1': ['val1'], 'val4': ['val1']}
# assert not list_sessions()
@ -1534,9 +1654,9 @@ def test_calc_value_join_multi_value(config_type):
od1 = OptionDescription('root', '', [val1, val2, val3, val4])
cfg = Config(od1)
cfg = get_config(cfg, config_type)
assert cfg.value.dict() == {'val1': ['val1'], 'val2': ['val2'], 'val3': [None], 'val4': []}
assert parse_od_get(cfg.value.get()) == {'val1': ['val1'], 'val2': ['val2'], 'val3': [None], 'val4': []}
cfg.option('val3').value.set(['val3'])
assert cfg.value.dict() == {'val1': ['val1'], 'val2': ['val2'], 'val3': ['val3'], 'val4': ['val1.val2.val3']}
assert parse_od_get(cfg.value.get()) == {'val1': ['val1'], 'val2': ['val2'], 'val3': ['val3'], 'val4': ['val1.val2.val3']}
# assert not list_sessions()
@ -1548,9 +1668,9 @@ def test_calc_value_min():
od1 = OptionDescription('root', '', [val1, val2, val3, val4])
cfg = Config(od1)
cfg.property.read_write()
assert cfg.value.dict() == {'val1': 'val1', 'val2': 'val2', 'val3': 'val3', 'val4': 'val1.val2.val3'}
assert parse_od_get(cfg.value.get()) == {'val1': 'val1', 'val2': 'val2', 'val3': 'val3', 'val4': 'val1.val2.val3'}
cfg.option('val3').property.add('disabled')
assert cfg.value.dict() == {'val1': 'val1', 'val2': 'val2', 'val4': ''}
assert parse_od_get(cfg.value.get()) == {'val1': 'val1', 'val2': 'val2', 'val4': ''}
# assert not list_sessions()
@ -1561,7 +1681,7 @@ def test_calc_value_add(config_type):
od1 = OptionDescription('root', '', [val1, val2, val3])
cfg = Config(od1)
cfg = get_config(cfg, config_type)
assert cfg.value.dict() == {'val1': 1, 'val2': 2, 'val3': 3}
assert parse_od_get(cfg.value.get()) == {'val1': 1, 'val2': 2, 'val3': 3}
# assert not list_sessions()
@ -1586,3 +1706,38 @@ def test_calc_dependencies(config_type):
def test_callback__kwargs_wrong(config_type):
with pytest.raises(ValueError):
Params(kwargs='string')
def test_callback_information_parent(config_type):
information = ParamInformation('information')
val1 = StrOption('val1', "", Calculation(return_value, Params(information)))
od2 = OptionDescription('od', '', [val1], informations={'information': 'new_value'})
information.set_option(od2)
od1 = OptionDescription('rootconfig', '', [od2])
cfg = Config(od1)
cfg.property.read_write()
cfg = get_config(cfg, config_type)
assert cfg.option('od.val1').value.get() == 'new_value'
cfg.option('od').information.set('information', 'new_value2')
assert cfg.option('od.val1').value.get() == 'new_value2'
def test_callback_information_redefined(config_type):
val1 = StrOption('val1', "")
information = ParamInformation('information', option=val1)
val2 = StrOption('val2', "", Calculation(return_value, Params(information)))
od2 = OptionDescription('od', '', [val1, val2], informations={'information': 'new_value'})
with pytest.raises(ConfigError):
information.set_option(od2)
def test_callback_information_redefined_after(config_type):
information = ParamInformation('information')
val1 = StrOption('val1', "", Calculation(return_value, Params(information)))
od2 = OptionDescription('od', '', [val1], informations={'information': 'new_value'})
od1 = OptionDescription('rootconfig', '', [od2])
cfg = Config(od1)
cfg.property.read_write()
cfg = get_config(cfg, config_type)
with pytest.raises(ConfigError):
information.set_option(od2)

View file

@ -115,8 +115,10 @@ def test_force_default_on_freeze_multi():
cfg_ori.property.read_write()
cfg = cfg_ori
# FIXME cfg = get_config(cfg_ori, config_type)
cfg.option('dummy1').value.set([undefined, True])
cfg.option('dummy2').value.set([undefined, False])
default = cfg.option('dummy1').value.default()[0]
cfg.option('dummy1').value.set([default, True])
default = cfg.option('dummy2').value.default()[0]
cfg.option('dummy2').value.set([default, False])
owner = cfg.owner.get()
assert cfg.option('dummy1').owner.get() == owner
assert cfg.option('dummy2').owner.get() == owner
@ -144,45 +146,23 @@ def test_force_default_on_freeze_multi():
# assert not list_sessions()
def test_force_default_on_freeze_leader():
dummy1 = BoolOption('dummy1', 'Test int option', multi=True, properties=('force_default_on_freeze',))
dummy2 = BoolOption('dummy2', 'Test string option', multi=True)
descr = Leadership("dummy1", "", [dummy1, dummy2])
od1 = OptionDescription("root", "", [descr])
with pytest.raises(ConfigError):
Config(od1)
#def test_force_default_on_freeze_leader():
# dummy1 = BoolOption('dummy1', 'Test int option', multi=True, properties=('force_default_on_freeze',))
# dummy2 = BoolOption('dummy2', 'Test string option', multi=True)
# descr = Leadership("dummy1", "", [dummy1, dummy2])
# od1 = OptionDescription("root", "", [descr])
# with pytest.raises(ConfigError):
# Config(od1)
# assert not list_sessions()
def test_force_metaconfig_on_freeze_leader():
dummy1 = BoolOption('dummy1', 'Test int option', multi=True, properties=('force_metaconfig_on_freeze',))
dummy2 = BoolOption('dummy2', 'Test string option', multi=True)
descr = Leadership("dummy1", "", [dummy1, dummy2])
od1 = OptionDescription("root", "", [descr])
with pytest.raises(ConfigError):
Config(od1)
# assert not list_sessions()
def test_force_default_on_freeze_leader_frozen():
dummy1 = BoolOption('dummy1', 'Test int option', multi=True, properties=('force_default_on_freeze', 'frozen'))
dummy2 = BoolOption('dummy2', 'Test string option', multi=True)
descr = Leadership("dummy1", "", [dummy1, dummy2])
od1 = OptionDescription("root", "", [descr])
cfg = Config(od1)
with pytest.raises(LeadershipError):
cfg.option('dummy1.dummy1').property.remove('frozen')
# assert not list_sessions()
def test_force_metaconfig_on_freeze_leader_frozen():
dummy1 = BoolOption('dummy1', 'Test int option', multi=True, properties=('force_metaconfig_on_freeze', 'frozen'))
dummy2 = BoolOption('dummy2', 'Test string option', multi=True)
descr = Leadership("dummy1", "", [dummy1, dummy2])
od1 = OptionDescription("root", "", [descr])
cfg = Config(od1)
with pytest.raises(LeadershipError):
cfg.option('dummy1.dummy1').property.remove('frozen')
#def test_force_metaconfig_on_freeze_leader():
# dummy1 = BoolOption('dummy1', 'Test int option', multi=True, properties=('force_metaconfig_on_freeze',))
# dummy2 = BoolOption('dummy2', 'Test string option', multi=True)
# descr = Leadership("dummy1", "", [dummy1, dummy2])
# od1 = OptionDescription("root", "", [descr])
# with pytest.raises(ConfigError):
# Config(od1)
# assert not list_sessions()

Some files were not shown because too many files have changed in this diff Show more