Skip to content

Commit

Permalink
Merge pull request #26 from srfoster65/experimental
Browse files Browse the repository at this point in the history
  • Loading branch information
srfoster65 authored Nov 2, 2023
2 parents 66bb5d1 + 83b2c3d commit 9b6b7cc
Show file tree
Hide file tree
Showing 25 changed files with 1,042 additions and 709 deletions.
54 changes: 32 additions & 22 deletions docs/reference.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,73 @@
# Reference

## ArgInit
## ClassArgInit

```python
ArgInit(env_prefix="", priority=ARG_PRIORITY, use_kwargs=False, func_is_bound=False, set_attrs=True, args=None)
ClassArgInit(env_prefix=None, priority=ENV_PRIORITY, use_kwargs=False, set_attrs=True, protect_atts=True, defaults=None)
```

Initialise arguments using the function that calls ArgInit as the reference. Process each argument, setting the value of the class dictionary, args, with either the value provided by the argument, an associated environment variable or a default value. If the value of the arg or env is None then try the next item in the selected priority system
Resolve argument values using the bound function that calls ClassArgInit as the reference. Process each argument (skipping the first argument as this is a class reference) from the calling function, resolving and storing the value in a dictionary, where the argument name is the key.

### Arguments

+ **env_prefix**: env_prefix is used to avoid namespace clashes with environment variables. If set, all environment variables must have include this prefix followed by an "_" character and the name of the argument.
+ **env_prefix**: env_prefix is used to avoid namespace clashes with environment variables. If set, all environment variables must include this prefix followed by an "_" character and the name of the argument.

+ **priority**: By default arguments will be set based on the prioty env, arg, default. An alternate priority of arg, env, default is available by setting priority=ARG_PRIORITY.
+ **priority**: By default arguments will be set based on the priority env, arg, default. An alternate priority of arg, env, default is available by setting priority=ARG_PRIORITY.

+ **use_kwargs**: When initialising arguments, only named arguments will be initialised by default. If use_kwargs=True, then any keyword arguments will also be initialised

+ **is_class**: Set to True if the function being processed is a class method i.e. the first argument is "self"
+ **set_attrs**: Set the arguments as class attributes. Default is true.

+ **set_attrs**: If the function being processed is a class method (a bound function), set the arguments as class attributes. Default is true. Set to false to disable. This attribute has no effect if is_class=False.
+ **protect_attrs**: Add a leading "_" character to all assigned attribute names. Default is True.

+ **args**: A list of Arg objects that allow overriding the processing behaviour of individual arguments.
+ **defaults**: A list of ArgDefault objects.

### Attributes

#### args

An object representing the processed arguments. Arguments are exposed as attributes or key/value pairs.
An object representing the resolved arguments. Arguments are exposed as attributes or key/value pairs.

Note: The returned object is a [python-box](https://github.com/cdgriffith/Box) Box class.

## Arg
## FunctionArgInit

```python
Arg(name, env=None, default=None, attr=None, force_arg=False, force_env=True, force_default=True, disable_env=False)
FunctionArgInit(env_prefix=None, priority=ENV_PRIORITY, use_kwargs=False, defaults=None)
```

A dataclass that is used to customise the processing of arguments.
Resolve argument values using the function that calls FunctionArgInit as the reference. Process each argument from the calling function, resolving and storing the value in a dictionary, where the argument name is the key.

### Arguments

+ **name**: (required) The name of the argument.
+ **env_prefix**: env_prefix is used to avoid namespace clashes with environment variables. If set, all environment variables must include this prefix followed by an "_" character and the name of the argument.

+ **env**: The name of the associated environment variable. Only required if different from the argument name. This value should not include any "env_prefix"
+ **priority**: By default arguments will be set based on the priority env, arg, default. An alternate priority of arg, env, default is available by setting priority=ARG_PRIORITY.

+ **default**: The default value to be applied if both arg and env values are not used
+ **use_kwargs**: When initialising arguments, only named arguments will be initialised by default. If use_kwargs=True, then any keyword arguments will also be initialised

+ **attr**: If set then the value of the argument in the args dictionary will use this name as the key instead of the arg name
+ **defaults**: A list of ArgDefault objects.

+ **force_arg**: If True, set the value if the arg value is None
### Attributes

+ **force_env**: If True, set the value if the env value is ""
#### args

+ **force_default**: If False, and the default value is None then no argument will be set in the args dictionary.
An object representing the resolved arguments. Arguments are exposed as attributes or key/value pairs.

## AttributeExistsError
Note: The returned object is a [python-box](https://github.com/cdgriffith/Box) Box class.

Raised if attempting to set an attribute of an object and an attribute with the same name already exists.
### ArgDefaults

```python
AttributeExistsError(Exception)
ArgDefaults(name, default_value=None, env_name="", disable_env=False)
```

A class that can be used to modify settings for an individual argument.

### Arguments

+ **env_name**: The name of the associated environment variable. If not set, env defaults to the uppercase equivalent of the argument name.

+ **default_value**: The default value to be applied if both arg and env values are not used.

+ **disable_env**: If True then do not consider the env value when resolving the value.
164 changes: 87 additions & 77 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,138 +8,148 @@ With Pip:
pip install arg_init
```

## Basic Usage
## Basic Usage for Class functions

ArgInit should be called from a function or class \_\_init\_\_() method that arguments should be processed for. ArgInit will determine the arguments of the calling function and resolve a value for each argument in turn from either an environment variable, the argument value or a default value.

All environment variables must begin with the prefix MYAPP_ to ensure there are no namespace clashes.
For Class methods: ClassArgInit() should be called from a class \_\_init\_\_() method that arguments should be processed for.

```python
from arg_init import ArgInit

def my_func(arg1=99):
args = ArgInit(env_prefix="MYAPP").args

my_func(101)
from arg_init import ClassArgInit

class MyClass:
def __init__(self, arg1=99):
ClassArgInit().args
print(self._arg1)
```

Running the above program would result in:

1. With a clean environment:
args.arg1 = 101
2. With the envirnment variable MYAPP_ARG1 set to "1":
args.arg1 = 1
3. With the program modified to call my_func():
args.arg1 = 99
Resolved arguments are exposed as protected class attributes e.g. "self._arg1".

### Overriding Default Argument Behaviour
## Basic Usage for simple functions

It is possible to override default behaviour per argument by providing a list of Arg objects at initialisation time.
FunctionArgInit() should be called from a function that arguments should be processed for.

```python
from arg_init import ArgInit, Arg
from arg_init import FunctionArgInit

def my_func(arg1):
arg_1 = Arg("arg1", force_arg=True)
args = ArgInit(env_prefix="MYAPP", args=[arg_1]).args
...
def my_func(arg1=99):
args = FunctionArgInit().args
print(args.arg1)
```

In this instance, no default is supplied for arg1 in the function definition as a value of **None** will be assigned in the absence of any env or argument being supplied.
Resolved arguments are exposed by accessing the args attribute of FunctionArgInit. Resolved values can be accessed as attributes e.g. args.arg1 or as a dictionary item e.g. args["arg1"].

## Use with a Class
## Other Use Cases

When used with a class ArgInit should be initialised with the argument func_is_bound=True. This notifies ArgInit that the first argument is a class reference and should not be processed.
Note: When used with classes, ArgInit is expected to be called from the \_\_init\_\_() method (But this is not a requirement).
### Setting a Common Prefix for all Environment Variables

To avoid namespace clashes with environment variables, it is recommneded to always supply an env_prefix argument when initialising ClassArgInit/FunctionArgInit. All environment variables are expected to have this prefix e.g. with an env_prefix of "myapp", arg1 would map to the environment variable "MYAPP_ARG1".

```text
env_prefix=<string>
```

env_prefix is converted to uppercase before use.

```python
from arg_init import ArgInit
from arg_init import ClassArgInit

class MyApp:
def __init__(self, arg1=None):
ArgInit(func_is_bound=True, env_prefix="myapp")
args = ClassArgInit(env_prefix="myapp").args
...

```

By default, ArgInit will set all arguments as class attributes of the MyApp instance. The negative to this implemntation is that linters will not recognise class attributes as being valid. e.g. Any references to self.arg1 in MyApp will be highlighted as invalid.
### Priority Modes

If this behaviour is not required set the argument set_attrs=False when initialising ArgInit.
Support for selecting the priority resolution mode is provided via the argument **priority**.

### Modifying Class Attributes Names
```text
priority=ENV_PRIORITY | ARG_PRIORITY
```

The names of the resolved arguments, and hence applied class attributes can be modified with the use of the Arg object and the attr argument.
By default, enviroment variables have priority over argument values. This can be changed at initialisation to give arguments prioirty.

```python
from arg_init import ArgInit, Arg

class MyApp:
def __init__(self, arg1=None):
args = [Arg("arg1", attr="_arg1")]
ArgInit(func_is_bound=True, env_prefix="myapp", args=args)
...
from arg_init import FunctionArgInit, ARG_PRIORITY

def my_func(arg1):
arg_init = FunctionArgInit()
args = arg_init.resolve(priority=ARG_PRIORITY)
...
```

In the example above, the instance of MyApp would have an attribute **_arg1** set with a value provided by resolving the arg1 argument.
Note: When using ARG_PRIORITY a default value should also be provided by ArgDefaults is a default value other than None is required.

## Support for kwargs
### Overriding Default Argument Behaviour

Support for kwargs in function signatures is provided via the argument **use_kwargs**.
It is possible to override default behaviour per argument using the ArgDefault object. A list of ArgDefaults objects can be passed into the call to ClassArgInit/FunctionArgInit.

ArgDefaults takes a "name" argumment and zero or more of the following optional arguments:

```python
from arg_init import ArgInit, Arg
+ default_value
+ env_name
+ disable_env

def my_func(self, **kwargs):
args = ArgInit(env_prefix="myapp", use_kwargs=True).args
...
#### default_value

fn = my_func({"test": "hello"})
When using ARG_Priority, the only way to set a default value is to the use ArgDefaults(default_value=value)

```
This can also be used when using ENV_Priority but the recommended solution is to use default python behaviour using function defaults e.g. fn(a=1).

Running the above program would result in fn.args.test being assigned the value "hello"
#### env_name

As before, environment variables will also be processed. If an envirnment variable MYAPP_TEST were assigned the value of "world", this would result in fn.args.test being assigned the value "world".
Setting this value allows a custom env name to be set as the lookup for an argument. This overrides the default setting and ignores any env prefix settings.

## Priority Modes
Note: env_name is converted to uppercase before use.

By default, enviroment variables have priority over argument values. This can be changed at initialisation to give arguments prioirty.
#### disable_env

Notice that the function arg1 default value is None, but the Arg default argument is set to "1".
If an argument should not use env value in its resolution process then set this attribute to True. If this attribute is set, even if the env exists it will not be used to resolve the argument value.

```python
from arg_init import ArgInit, Arg
#### Example using ArgDefaults

def my_func(arg1=None):
arg_1 = Arg("arg1", default=1)
args = ArgInit(env_prefix="MYAPP", priority=ArgInit.ARG_PRIORITY, args=[arg_1]).args
return args
In the example below, arg1 is modified to have a default value of 1 and to resolve from the environmnet variable "ALT_NAME"

print(my_func(10).arg1)
```python
from arg_init import FunctionArgInit, ArgDefaults

def func(arg1=None):
arg1_defaults = ArgDefaults(name="arg1", default_value=1, env_name="ALT_NAME")
args = FunctionArgInit(defaults=[arg1_defaults]).args
...
```

The example above will display the value "10" when run.
### Use with a Class

If the environment variable "MYAPP_ARG!" is set to "hello world" and the program is run again, it will still display "10", as arguments have priority over environment variables.
There are two additional class specific configuration options available:

If the program is modified to not pass any arguments into the call to my_func() as shown below:
+ set_attrs: default=True
+ protect_attr: default=True

```python
from arg_init import ArgInit, Arg
By default, ClassArgInit will set attributes directly on the calling class, using the argument name, with an "_" prefix for each argument in the calling functions' signature.

Setting set_attrs to False will prevent ClassArgInit from setting these class attributes.

def my_func(arg1=None):
arg_1 = Arg("arg1", default=1)
args = ArgInit(env_prefix="MYAPP", priority=ArgInit.ARG_PRIORITY, args=[arg_1]).args
return args
Setting protect_attrs to False will cause the attributes to be set using the argument name, without a leading "_" character.

print(my_func().arg1)
```python
from arg_init import ArgInit

class MyApp:
def __init__(self, arg1=None):
ClassArgInit(set_attrs=True, protect_attrs=False)
...
```

Assuming the environment variable "MYAPP_ARG!" is set to "hello world", the program will print out "hello world" when run.
By default, ClassArgInit will set all arguments as protected class attributes of the MyApp instance. In the above example, arg1 will be available as an attribute "arg1" of the instance of MyApp.

### Support for kwargs

If the environment variable is unset, and the program run a second time, it will print out "1"; The default value assigned to the Arg object associated with arg1.
Support for kwargs in function signatures is provided via the argument **use_kwargs**. When this argument is set, any keword arguments would be initialised using the same resolution process as named arguments.

```python
from arg_init import FunctionArgInit

def my_func(self, **kwargs):
args = FunctionArgInit(use_kwargs=True)
...
```
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ disable = []

[tool.pytest.ini_options]
minversion = "6.0"
addopts = "--cov=src"
addopts = "--cov=src --cov-report term-missing"
testpaths = [
"tests",
]
Loading

0 comments on commit 9b6b7cc

Please sign in to comment.