Skip to content

Commit

Permalink
Environment variable management (#53)
Browse files Browse the repository at this point in the history
* pyapp variables: remove from init, move in toml to `env-vars` section

* Allow setting of string, int, and bool variables

* Add possibility to get a variable

* BF in uninitialize

* Unset variables, list variables

* rename function about pyapp variablle -> it's for all env vars

* add tests for case where unsetting a non-existing variable
  • Loading branch information
trappitsch authored Aug 30, 2024
1 parent 1e25368 commit 0d63d91
Show file tree
Hide file tree
Showing 14 changed files with 525 additions and 96 deletions.
17 changes: 17 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
## v0.4.0

**If you are setting PyApp variables, this is a breaking change!**

- Move environmental variables to their own table in `pyproject.toml`. They are now in `[tool.box.env-vars]`.
- Remove PyApp Variables from initialization and put them into their own command.
- Add a new command `box env` in order to manage environmental variables.
- Add `box env --set KEY=VALUE` to add string variables.
- Add `box env --set-int KEY=VALUE` to add integer variables.
- Add `box env --set-bool KEY=VALUE` to add boolean variables.
- Add `box env --get VAR_NAME` to get the value of a variable.
- Add `box env --unset VAR_NAME` to remove a variable.
- Add `box env --list` to list all variables.
- Bug fix for `box uninit`: Will throw a useful error if not in a `box` project.

If this breaks your project, you can either run `box uninit` followed by `box init` and re-enter the variables, or you can manually edit the `pyproject.toml` file.

## v0.3.0

- Fix linux GUI uninstaller, such that it will only delete the installation folder if it is empty.
Expand Down
1 change: 0 additions & 1 deletion docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ a simple PyQt6 wrapper around
4. Provide the app entry point: The automatically selected point is correct, so we choose from the list - option 0.
5. Choose the App entry type according to PyApp's options: Here we use `spec`.
6. Provide the Python version to package with: Here we use the default `3.12`.
7. Provide additonal PyApp variables: Not required here (default).
2. After successfull initialization, we package the project with `box package`. This first builds the package and then packages it with PyApp. Hold on...
3. The executable is now in `target/release`.
4. Run the executable!
Expand Down
45 changes: 44 additions & 1 deletion docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ The following table shows a list of questions, their default values, their argum
| Please type an app entry for the project or choose one from the list below | First entry in `pyproject.toml` for `[project.gui-scripts]`, if not available, then for `[project.scripts]`. If no entry points are given, the default value is set to `package_name:run`. | `-e`<br>`--entry` | *Required:* The entry point for the application. This is the command that will be used to start the application. If you have a `pyproject.toml` file, `box` will try and read potential entry points that you can select by passing it the digit of the list. You can also provide an entry point manually by typing it here. |
| Choose an entry type for the project. | `spec` | `-et`<br>`--entry-type` | *Required:* This specifies the type of the entry point that `PyApp` will use. `spec` (default) is an object reference, `module` for a python module, `script` for a python script, and `notebook` for a jupyter notebook. Details can be found [here](https://ofek.dev/pyapp/latest/config/#execution-mode). |
| Choose a python version to package the project with. | `3.12` | `--py`<br>`--python-version` | *Required:* The python version to package with your project. You can choose any python version from the list. More details can be found on the `PyApp` website [here](https://ofek.dev/pyapp/latest/config/#python-distribution). |
| Optional PyApp variables | `None` | `--opt-pyapp-vars` | *Optional:* Set any optional environmental variables that you can use in `PyApp` here by giving a list of variable name followed by the value for that variable. Note that the total number of arguments must be even. See the [`PyApp` documentation](https://ofek.dev/pyapp) for more information. If there are no optional variables, just hit enter. |

If you provided all the answers, your project should have been successfully initialized and print an according message.

Expand All @@ -41,6 +40,50 @@ If you provided all the answers, your project should have been successfully init
If you re-initalize a project, `box` will use the already set values as proposed default values.
If you re-initialize in quiet mode and just give one new option, all other options will stay the same.

## Manage environmental variables

PyApp uses environmental variables for all configurations.
While `box` includes the basics of PyApp configuration,
you might want to set additional environmental variables.
This is done with the `box env` command.

### Set an environmental variable

You can set three types of environmental variables:

- `--set KEY=VALUE` to set a string variable.
- `--set-int KEY=VALUE` to set an integer variable.
- `--set-bool KEY=VALUE` to set a boolean variable.

For example, to set a string variable `MY_VAR` to `my_value`, type:

```
box env --set MY_VAR=my_value
```

### Get an environmental variable

Once set, you can simply get an environmental variable by typing:

```
box env --get VARIABLE_NAME
```

If not variable with this name is defined, a warning will be printed.
To list all currently set variables, type:

```
box env --list
```

### Unset a variable

To unset a variable, type:

```
box env --unset VARIABLE_NAME
```

## Packaging

To package your project, simply run:
Expand Down
57 changes: 46 additions & 11 deletions src/box/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import rich_click as click

import box
from box import env_vars
from box.cleaner import CleanProject
from box.config import uninitialize
from box.initialization import InitializeProject
Expand Down Expand Up @@ -46,14 +47,6 @@ def cli():
default=None,
help="Set the project as a GUI project. In quiet mode, this will default to `False`.",
)
@click.option(
"--opt-pyapp-vars",
help=(
"Set optional PyApp variables for the project. "
"`Example: PYAPP_FULL_ISOLATION 1`"
),
type=str,
)
@click.option("-e", "--entry", help="Set the app entry for the project.")
@click.option(
"-et",
Expand All @@ -77,7 +70,6 @@ def init(
entry,
entry_type,
python_version,
opt_pyapp_vars,
):
"""Initialize a new project in the current folder."""
ut.check_pyproject()
Expand All @@ -89,11 +81,54 @@ def init(
app_entry=entry,
app_entry_type=entry_type,
python_version=python_version,
opt_pyapp_vars=opt_pyapp_vars,
)
my_init.initialize()


@cli.command(name="env")
@click.option(
"--get", "get_var", help="Get the value that is currently set to a variable."
)
@click.option("--list", "list_vars", is_flag=True, help="List all variables set.")
@click.option(
"--set",
"set_string",
help=("Set a `key=value` environmental variable pair with a string value."),
)
@click.option(
"--set-bool",
help=(
"Set a `key=value` environmental variable pair with a boolean. "
"Valid boolean values are `0`, `1`, `True`, `False` (case insensitive)."
),
)
@click.option(
"--set-int",
help=("Set a `key=value` environmental variable pair with an integer value."),
)
@click.option("--unset", help="Unset variable with a given name.")
def env(get_var, list_vars, set_bool, set_int, set_string, unset):
"""Manage the environmental variables.
All environmental variables will be set when packaging the app with PyApp.
Therefore, if you want to set specific PYAPP_X variables, set them here.
"""
ut.check_boxproject()

if get_var:
env_vars.get_var(get_var)
if set_bool:
env_vars.set_bool(set_bool)
if set_int:
env_vars.set_int(set_int)
if set_string:
env_vars.set_string(set_string)
if unset:
env_vars.unset(unset)
if list_vars:
env_vars.get_list()


@cli.command(name="package")
@click.option(
"-v",
Expand Down Expand Up @@ -236,7 +271,7 @@ def uninit(clean_project):
All references to `box` will be removed from the `pyproject.toml` file.
"""
ut.check_pyproject()
ut.check_boxproject()
if clean_project:
clean()
uninitialize()
Expand Down
61 changes: 51 additions & 10 deletions src/box/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ def builder(self) -> str:
"""Return the builder of the project."""
return self._pyproject["tool"]["box"]["builder"]

@property
def env_vars(self) -> Dict:
"""Return optional pyapp variables as list (if set), otherwise empty dict."""
try:
return self._pyproject["tool"]["box"]["env-vars"]
except (KeyError, TypeError):
return dict()

@property
def is_box_project(self):
"""Return if this folder is a box project or not."""
Expand Down Expand Up @@ -72,14 +80,6 @@ def optional_dependencies(self) -> Union[str, None]:
except KeyError:
return None

@property
def optional_pyapp_variables(self) -> Dict:
"""Return optional pyapp variables as list (if set), otherwise empty dict."""
try:
return self._pyproject["tool"]["box"]["optional_pyapp_vars"]
except KeyError:
return {}

@property
def possible_app_entries(self) -> OrderedDict:
"""Return [project.gui-scripts] or [project.scripts] entry if available.
Expand Down Expand Up @@ -124,12 +124,17 @@ def version(self) -> str:
return self._project["version"]


def pyproject_writer(key: str, value: Any) -> None:
def pyproject_writer(key: str, value: Any, category: str = None) -> None:
"""Modify the existing `pyproject.toml` file using `tomlkit`.
Project specific, the table [tools.box] is used. If the table does not exist,
it is created. If the key does not exist, it is created. If the key exists,
it is overwritten.
:param key: Key to write to.
:param value: Value to write to key.
:param category: If given, will write to ["tool"]["box"]["category"]["key"]["value"]
"""
pyproject_file = Path("pyproject.toml")
if not pyproject_file.is_file():
Expand Down Expand Up @@ -161,7 +166,22 @@ def pyproject_writer(key: str, value: Any) -> None:
tool_table.append("box", box_table)
doc.add("tool", tool_table)

box_table.update({key: value})
if not category:
edit_table = box_table
else:
try:
edit_table = doc["tool"]["box"][category]
except KeyError:
doc.add(tomlkit.nl())
tool_table = tomlkit.table(True)
category_table = tomlkit.table(True)
tool_table.append("box", category_table)
category_table.add(tomlkit.nl())
edit_table = tomlkit.table()
category_table.append(category, edit_table)
doc.add("tool", tool_table)

edit_table.update({key: value})

with open(pyproject_file, "w", newline="\n") as f:
tomlkit.dump(doc, f)
Expand All @@ -178,3 +198,24 @@ def uninitialize() -> None:

with open(pyproject_file, "w", newline="\n") as f:
tomlkit.dump(doc, f)


def unset_env_variable(var_name: str) -> bool:
"""Unset a variable name and return status if done or not.
:param var_name: Variable name in ["tool.box.env-vars"]
:return: True if variable successfully unset, False if not found, otherwise.
"""
pyproject_file = Path("pyproject.toml")

with open(pyproject_file, "rb") as f:
doc = tomlkit.load(f)

try:
doc["tool"]["box"]["env-vars"].remove(var_name)
with open(pyproject_file, "w", newline="\n") as f:
tomlkit.dump(doc, f)
return True
except: # noqa: E722
return False
Loading

0 comments on commit 0d63d91

Please sign in to comment.