Skip to content

Commit

Permalink
add test and publish
Browse files Browse the repository at this point in the history
  • Loading branch information
Joaopeuko committed Apr 27, 2024
1 parent 4bfba04 commit 55ba0a3
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 11 deletions.
44 changes: 44 additions & 0 deletions .github/workflows/publish-pypi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Publish Pypi

on:
push:
branches:
- '*'
tags:
- v*

jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade setuptools
python -m pip install poetry
python -m pip install twine
python -m poetry install
- name: Build and publish to Test PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.TEST_PYPI }}
run: |
python -m poetry build
twine upload --skip-existing --repository-url https://test.pypi.org/legacy/ dist/*
- name: Publish to PyPI (if it's a new tag)
if: github.ref == 'refs/heads/master'
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
python -m poetry build
twine upload dist/*
File renamed without changes.
97 changes: 96 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pyyaml = "^6.0.1"
pre-commit = "^3.7.0"
python-dotenv = "^1.0.1"
tomlkit = "^0.12.4"
pytest = "^8.1.2"

[build-system]
requires = ["poetry-core"]
Expand Down
2 changes: 1 addition & 1 deletion secured/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def _convert_value(self, value: dict | str) -> Secure | str: # type: ignore
The converted value, secured if `secure` is True and not a dictionary.
"""
if isinstance(value, dict):
return AttrDict(value, secure=self.secure, message=self.message)
return AttrDict(value, secure=self.secure, message=self.message) # type: ignore
elif self.secure:
return Secure(value, self.message)
return value
Expand Down
18 changes: 9 additions & 9 deletions secured/secured.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
import yaml # type: ignore
from typing import List
from typing import List, Dict, Any

from .log_manager import setup_default_logger
from .secure import Secure
Expand Down Expand Up @@ -54,7 +54,7 @@ def load_yaml(self, yaml_paths: str | List[str], secure: bool) -> None:
self.logger.error(f"Error parsing YAML file {path}: {e}")
continue

def create_config(self, data: dict[str, dict], secure: bool) -> AttrDict | dict[str, Secure]:
def create_config(self, data: Dict[str, Any], secure: bool) -> Dict[str, Any]: # type: ignore
"""
Create a configuration from data loaded from a YAML file.
Expand All @@ -71,15 +71,15 @@ def create_config(self, data: dict[str, dict], secure: bool) -> AttrDict | dict[
return {key: Secure(val, self.message) if secure and not isinstance(val, dict) else val
for key, val in self._recursive_dict(data).items()}

def _recursive_dict(self, data: dict) -> dict:
def _recursive_dict(self, data: Dict[str, Any]) -> Dict[str, Any]: # type: ignore[type-arg]
"""
Recursively parse and secure dictionary data.
Args:
data (dict): Data to parse.
data: Data to parse.
Returns:
dict: Parsed data, potentially secured.
Parsed data, potentially secured.
"""
return {key: self._recursive_dict(val) if isinstance(val, dict) else val for key, val in data.items()}

Expand All @@ -88,8 +88,8 @@ def get(self, key: str, required: bool = False) -> Secure | None:
Retrieve configuration value by key, securing it.
Args:
key (str): The key for the configuration value.
required (bool, optional): Whether the key is required (raises an error if not found).
key: The key for the configuration value.
required: Whether the key is required (raises an error if not found).
Returns:
The value associated with the key, secured.
Expand All @@ -109,9 +109,9 @@ def use_attrdict(self, use: bool) -> None:
Toggle the use of AttrDict for storing data.
Args:
use (bool): Flag indicating whether to use AttrDict.
use: Flag indicating whether to use AttrDict.
"""
self.as_attrdict = use
for key, value in self.__dict__.items():
if isinstance(value, (AttrDict, dict)):
self.__dict__[key] = AttrDict(value, secure=value.secure) if use else dict(value)
self.__dict__[key] = AttrDict(value, secure=self.secure) if use else dict(value) # type: ignore
40 changes: 40 additions & 0 deletions tests/test_attribute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pytest
from secured.attribute import AttrDict
from secured.secure import Secure

def test_attribute_access():
"""Test attribute-style access to dictionary keys."""
ad = AttrDict({'key1': 'value1', 'key2': 'value2'})
assert ad.key1 == 'value1'
assert ad.key2 == 'value2'

def test_secure_value_access():
"""Test that values are secured correctly when the secure flag is True."""
ad = AttrDict({'password': 'my_secret'}, secure=True, message="<Custom Secured>")
assert isinstance(ad.password, Secure)
assert str(ad.password) == "<Custom Secured>"

def test_nested_dict_conversion():
"""Test that nested dictionaries are converted into AttrDict instances."""
ad = AttrDict({'nested': {'key': 'value'}}, secure=False)
assert isinstance(ad.nested, AttrDict)
assert ad.nested.key == 'value'

def test_setattr_behavior():
"""Test setting attributes using dot notation."""
ad = AttrDict(secure=False)
ad.new_key = 'new_value'
assert 'new_key' in ad
assert ad['new_key'] == 'new_value'

def test_secure_flag_inheritance():
"""Test that the secure flag is inherited by nested AttrDict instances."""
ad = AttrDict({'nested': {'key': 'value'}}, secure=True, message="<Custom Secured>")
assert isinstance(ad.nested.key, Secure)
assert str(ad.nested.key) == "<Custom Secured>"

def test_exception_for_nonexistent_attribute():
"""Test that accessing a nonexistent attribute raises an AttributeError."""
ad = AttrDict(secure=False)
with pytest.raises(AttributeError):
_ = ad.nonexistent
Empty file added tests/test_secure.py
Empty file.
Empty file added tests/test_secured.py
Empty file.

0 comments on commit 55ba0a3

Please sign in to comment.