Skip to content

Commit

Permalink
Add util.set_list (#83)
Browse files Browse the repository at this point in the history
* add util.set_list

* update dependencies

* update supported python versions

* update codecov github wokflow section
  • Loading branch information
jason20c authored Feb 20, 2024
1 parent bcecbdf commit 474c88b
Show file tree
Hide file tree
Showing 7 changed files with 1,033 additions and 835 deletions.
12 changes: 4 additions & 8 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,18 @@ jobs:
fail-fast: false
matrix:
os: [ "ubuntu-latest", "macos-latest" ]
python-version: [ "3.7", "3.8", "3.9", "3.10" ]
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ]
runs-on: ${{ matrix.os }}
steps:
- name: Check out repository
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install virtualenv from poetry
uses: 20c/workflows/poetry@v1
with:
python-version: ${{ matrix.python-version }}
- name: Run tests
run: |
poetry run tox
poetry run coverage report
# upload coverage stats
run: poetry run tox -e py
- name: Upload coverage
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
fail_ci_if_error: true
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ repos:
hooks:
- id: pyupgrade
name: pyupgrade
entry: poetry run pyupgrade --py36-plus
entry: poetry run pyupgrade --py38-plus
language: python
types: [python]
pass_filenames: true
Expand Down
1,707 changes: 894 additions & 813 deletions poetry.lock

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ license = "Apache-2.0"
classifiers = [
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.7",
"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",
]

packages = [{ include = "confu", from = "src" }]

[tool.poetry.dependencies]
python = "^3.7"
python = "^3.8"
munge = "^1.2.1"

[tool.poetry.dev-dependencies]
Expand All @@ -30,11 +31,11 @@ click = ">=7,<8"

# lint
black = { version = ">=20", allow-prereleases = true }
isort = "^5.7.0"
flake8 = "^3.8.4"
mypy = "^0.950"
pre-commit = "^2.13"
pyupgrade = "^2.19.4"
isort = ">=5.7.0"
flake8 = ">=3.8.4"
mypy = ">=0.950"
pre-commit = ">=2.13"
pyupgrade = ">=2.19.4"

# ctl
ctl = "^1"
Expand Down
46 changes: 46 additions & 0 deletions src/confu/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,52 @@ def set_option(
else:
self.set_default(name, value)

def set_list(
self,
name: str,
value: list[str],
envvar_element_type: type | None = None,
delimiter: str = ",",
) -> None:
"""
Sets an option to be a list by first checking for environment variables,
then checking for value already set,
then going to the `value` argument passed.
Environment variable values are split by commas.
Supports only single level single type lists.
The `value` arguments's first element's type is used to coerce the environment variable's
value to the correct type.
If the value passed is `None`, or an empty list, then the optional element_type argument
is used to coerce the list elements to the correct type.
**Arguments**
- name (`str`)
- value
**Keyword Arguments**
- envvar_element_type
- delimiter
"""
if value is not None and len(value) > 0:
envvar_element_type = type(value[0])
else:
# If value is None or value array is empty, we'll use the provided envvar_type, if it is not None
if envvar_element_type is None:
raise ValueError(
f"If no default value is provided for the setting {name} the envvar_element_type argument must be set."
)

if name in os.environ:
array = os.environ.get(name).split(delimiter)
# coerce elements to the correct type
self.scope[name] = [envvar_element_type(element) for element in array]
else:
self.set_default(name, value)

def set_bool(self, name: str, value: bool) -> None:
"""
Sets an option by first checking for environment variables,
Expand Down
77 changes: 75 additions & 2 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ def test_set_option_coerce_env_var(envvar_fixture):


def test_set_option_booleans(envvar_fixture):

scope = {}
settings_manager = SettingsManager(scope)
# env variables can only be set as strings
Expand Down Expand Up @@ -178,7 +177,7 @@ def test_set_bool(envvar_fixture):

def test_set_options_none(envvar_fixture):
"""
We coerce the environment variable to a boolean
We coerce the environment variable based on the envvar_type
"""
scope = {}
settings_manager = SettingsManager(scope)
Expand Down Expand Up @@ -225,3 +224,77 @@ def test_try_include_non_default():
env_file = os.path.join(os.path.dirname(__file__), "dev-non-default.py")
settings.try_include(env_file)
assert scope["TEST_SETTING"] == "hello"


def test_set_list(envvar_fixture):
"""
Setting arrays from environment variables
"""
scope = {}
settings_manager = SettingsManager(scope)

settings_manager.set_list("TEST_ARRAY", ["3", "4", "5"])
assert scope["TEST_ARRAY"] == ["3", "4", "5"]


def test_set_list_w_env_var(envvar_fixture):
"""
Environment variables take precedence over provided options
"""
scope = {}
settings_manager = SettingsManager(scope)

os.environ["TEST_SETTING"] = "0,1,2"
settings_manager.set_list("TEST_SETTING", ["3", "4", "5"])
assert scope["TEST_SETTING"] == ["0", "1", "2"]


def test_set_list_coerce(envvar_fixture):
"""
We coerce the environment variable to the same type
as the provided default.
"""
scope = {}
settings_manager = SettingsManager(scope)

os.environ["TEST_SETTING"] = "0,1,2"
settings_manager.set_list("TEST_SETTING", [3, 4, 5])
assert scope["TEST_SETTING"] == [0, 1, 2]


def test_set_list_none(envvar_fixture):
"""
We coerce the environment variable to the envvar_element_type
if the value is None or an empty list
"""
scope = {}
settings_manager = SettingsManager(scope)

with pytest.raises(ValueError):
settings_manager.set_list(
"TEST_SETTING",
None,
)

os.environ["TEST_SETTING"] = "0,1,2"
settings_manager.set_list("TEST_SETTING", None, envvar_element_type=int)
assert scope["TEST_SETTING"] == [0, 1, 2]

settings_manager.set_list("TEST_SETTING", [], envvar_element_type=int)
assert scope["TEST_SETTING"] == [0, 1, 2]

# value type should take precedence
settings_manager.set_list("TEST_SETTING", [""], envvar_element_type=int)
assert scope["TEST_SETTING"] == ["0", "1", "2"]


def test_set_list_delimiter(envvar_fixture):
"""
We specify a delimiter to split the environment variable
"""
scope = {}
settings_manager = SettingsManager(scope)

os.environ["TEST_SETTING"] = "0 1 2"
settings_manager.set_list("TEST_SETTING", [""], delimiter=" ")
assert scope["TEST_SETTING"] == ["0", "1", "2"]
9 changes: 5 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[flake8]
extend-ignore = E501
extend-ignore = E501 W503
per-file-ignores =
# Undefined name
./tests/dev-default.py: F821
Expand All @@ -22,16 +22,17 @@ select = B,C,E,F,W,T4,B9

[gh-actions]
python =
3.7: py37
3.8: py38
3.9: py39
3.10: py310
3.11: py311
3.12: py312

[pytest]
norecursedirs = .Ctl data gen .tox

[tox]
envlist = py{37,38,39,310}
envlist = py{38,39,310,311,312}
isolated_build = True

[testenv]
Expand All @@ -41,4 +42,4 @@ deps =
poetry
commands =
poetry install -v
poetry run pytest tests/ --cov={envsitepackagesdir}/confu --cov-report=xml
poetry run pytest -vs --cov={toxinidir}/src --cov-report=term-missing --cov-report=xml tests/

0 comments on commit 474c88b

Please sign in to comment.