From 2ca79c8f08713333fef646e8e90e63e974ddc683 Mon Sep 17 00:00:00 2001 From: Ryan Galloway Date: Wed, 1 Apr 2026 21:09:11 -0700 Subject: [PATCH 1/5] Move return outside of finally block --- lib/envstack/encrypt.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/envstack/encrypt.py b/lib/envstack/encrypt.py index 7b99fce..d0d6b86 100644 --- a/lib/envstack/encrypt.py +++ b/lib/envstack/encrypt.py @@ -121,8 +121,7 @@ def encrypt(self, data: str): log.error("invalid value: %s", e) except Exception as e: log.error("unhandled error: %s", e) - finally: - return results + return results def decrypt(self, data: str): """Decrypt a secret using Fernet. @@ -224,8 +223,7 @@ def encrypt(self, data: str): log.error("invalid value: %s", e) except Exception as e: log.error("unhandled error: %s", e) - finally: - return results + return results def decrypt(self, data: str): """Convenience function to decrypt a secret using AES-GCM. From 4dd1ea03357c2dff8dc06a41e28b8376eb01a302 Mon Sep 17 00:00:00 2001 From: Ryan Galloway Date: Wed, 1 Apr 2026 21:18:52 -0700 Subject: [PATCH 2/5] Fix Python 3.12+ warnings and add local pytest target --- CHANGELOG.md | 11 +++++++++++ Makefile | 7 ++++++- lib/envstack/encrypt.py | 12 +++++++++--- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbb3d23..95474a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [1.0.2] - 2026-04-01 + +### Changed +- add a `make pytest` target that installs the package in editable mode before running the test suite + +### Fixed +- remove `return` statements from `finally` blocks in encryption helpers to avoid newer Python `SyntaxWarning`s +- harden optional `cryptography` imports so missing crypto dependencies do not break exception handling paths + +--- + ## [1.0.1] - 2026-02-08 ### Fixed diff --git a/Makefile b/Makefile index e14dc22..b2cf987 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,11 @@ test: $(ENVSTACK_CMD) -- ls -al ${ENVSTACK_CMD} -- which python +# Run the pytest suite from an editable install, matching CI behavior +pytest: + python -m pip install -e . + pytest tests -q + # Install dryrun target to simulate installation dryrun: $(ENVSTACK_CMD) -- dist --dryrun @@ -51,4 +56,4 @@ install: build dist --force --yes # Phony targets -.PHONY: build dryrun install clean +.PHONY: build dryrun install clean test pytest diff --git a/lib/envstack/encrypt.py b/lib/envstack/encrypt.py index d0d6b86..047dc67 100644 --- a/lib/envstack/encrypt.py +++ b/lib/envstack/encrypt.py @@ -44,9 +44,15 @@ # cryptography and _rust dependency may not be available everywhere # ImportError: DLL load failed while importing _rust: Module not found. Fernet = None +InvalidToken = type("InvalidToken", (Exception,), {}) +InvalidTag = type("InvalidTag", (Exception,), {}) +padding = None +Cipher = None +algorithms = None +modes = None try: - import cryptography.exceptions from cryptography.fernet import Fernet, InvalidToken + from cryptography.exceptions import InvalidTag from cryptography.hazmat.primitives import padding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes except ImportError as err: @@ -217,7 +223,7 @@ def encrypt(self, data: str): results = compact_store(encrypted_data) except binascii.Error as e: log.error("invalid base64 encoding: %s", e) - except cryptography.exceptions.InvalidTag: + except InvalidTag: log.error("invalid encryption key") except ValueError as e: log.error("invalid value: %s", e) @@ -237,7 +243,7 @@ def decrypt(self, data: str): return decrypted.decode() except binascii.Error as e: log.debug("invalid base64 encoding: %s", e) - except cryptography.exceptions.InvalidTag: + except InvalidTag: log.debug("invalid encryption key") except ValueError as e: log.debug("invalid value: %s", e) From f0ec3a222e32a866b75659c7325c8f1d1b1966df Mon Sep 17 00:00:00 2001 From: Ryan Galloway Date: Wed, 1 Apr 2026 21:19:34 -0700 Subject: [PATCH 3/5] Prepare version 1.0.2 --- lib/envstack/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/envstack/__init__.py b/lib/envstack/__init__.py index a75af53..8d9385a 100644 --- a/lib/envstack/__init__.py +++ b/lib/envstack/__init__.py @@ -34,7 +34,7 @@ """ __prog__ = "envstack" -__version__ = "1.0.1" +__version__ = "1.0.2" from envstack.env import clear, init, revert, save # noqa: F401 from envstack.env import load_environ, resolve_environ # noqa: F401 diff --git a/pyproject.toml b/pyproject.toml index 0abb5e1..33528f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "envstack" -version = "1.0.1" +version = "1.0.2" description = "Environment variable composition layer for tools and processes." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.6" From 794448206888eaa875e3441af0e3053dc44e774d Mon Sep 17 00:00:00 2001 From: Ryan Galloway Date: Thu, 2 Apr 2026 05:22:46 -0700 Subject: [PATCH 4/5] Harden crypto fallbacks and expand local/CI test coverage --- .github/workflows/tests.yml | 3 ++- CHANGELOG.md | 5 +++-- Makefile | 10 +++------- lib/envstack/encrypt.py | 23 +++++++++++++++++++++++ 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 272f8c9..9b16a5c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,11 +8,12 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: ${{ matrix.python-version }} - run: python -m pip install --upgrade pip - run: pip install pytest - run: pip install -e . diff --git a/CHANGELOG.md b/CHANGELOG.md index 95474a6..c185d42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.2] - 2026-04-01 ### Changed -- add a `make pytest` target that installs the package in editable mode before running the test suite +- add a self-contained `make test` target that installs `pytest` and the package in editable mode before running the test suite +- mark `all` as a phony Make target to avoid filename collisions ### Fixed - remove `return` statements from `finally` blocks in encryption helpers to avoid newer Python `SyntaxWarning`s -- harden optional `cryptography` imports so missing crypto dependencies do not break exception handling paths +- harden optional `cryptography` imports with a decorator-based availability guard so missing crypto dependencies raise a clear runtime error instead of `NoneType` failures --- diff --git a/Makefile b/Makefile index b2cf987..818506f 100644 --- a/Makefile +++ b/Makefile @@ -36,13 +36,9 @@ build: clean # Combined target to build for both platforms all: build -# Test target to verify the build -test: - $(ENVSTACK_CMD) -- ls -al - ${ENVSTACK_CMD} -- which python - # Run the pytest suite from an editable install, matching CI behavior -pytest: +test: + python -m pip install pytest python -m pip install -e . pytest tests -q @@ -56,4 +52,4 @@ install: build dist --force --yes # Phony targets -.PHONY: build dryrun install clean test pytest +.PHONY: all build dryrun install clean test pytest diff --git a/lib/envstack/encrypt.py b/lib/envstack/encrypt.py index 047dc67..3cb6642 100644 --- a/lib/envstack/encrypt.py +++ b/lib/envstack/encrypt.py @@ -38,11 +38,13 @@ import os import secrets from base64 import b64decode, b64encode +from functools import wraps from envstack.logger import log # cryptography and _rust dependency may not be available everywhere # ImportError: DLL load failed while importing _rust: Module not found. +CRYPTOGRAPHY_AVAILABLE = False Fernet = None InvalidToken = type("InvalidToken", (Exception,), {}) InvalidTag = type("InvalidTag", (Exception,), {}) @@ -55,10 +57,23 @@ from cryptography.exceptions import InvalidTag from cryptography.hazmat.primitives import padding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + CRYPTOGRAPHY_AVAILABLE = True except ImportError as err: log.debug("cryptography module not available: %s", err) +def require_cryptography(func): + """Guard crypto-backed functions when cryptography is unavailable.""" + + @wraps(func) + def wrapper(*args, **kwargs): + if not CRYPTOGRAPHY_AVAILABLE: + raise RuntimeError("cryptography support is not available") + return func(*args, **kwargs) + + return wrapper + + class Base64Encryptor(object): """Encrypt and decrypt secrets using base64 encoding.""" @@ -90,6 +105,7 @@ def __init__(self, key: str = None, env: dict = os.environ): self.key = self.get_key(env) @classmethod + @require_cryptography def generate_key(csl): """Generate a new 256-bit encryption key.""" if Fernet: @@ -110,6 +126,7 @@ def get_key(self, env: dict = os.environ): return Fernet(key) return key + @require_cryptography def encrypt(self, data: str): """Encrypt a secret using Fernet. @@ -129,6 +146,7 @@ def encrypt(self, data: str): log.error("unhandled error: %s", e) return results + @require_cryptography def decrypt(self, data: str): """Decrypt a secret using Fernet. @@ -158,6 +176,7 @@ def __init__(self, key: str = None, env: dict = os.environ): self.key = self.get_key(env) @classmethod + @require_cryptography def generate_key(csl): """Generate a new 256-bit encryption key.""" key = secrets.token_bytes(32) @@ -176,6 +195,7 @@ def get_key(self, env: dict = os.environ): raise ValueError("invalid base64 encoding: %s" % e) return key + @require_cryptography def encrypt_data(self, secret: str): """Encrypt a secret using AES-GCM. @@ -194,6 +214,7 @@ def encrypt_data(self, secret: str): "tag": b64encode(encryptor.tag).decode(), } + @require_cryptography def decrypt_data(self, encrypted_data: dict): """Decrypt a secret using AES-GCM. @@ -252,6 +273,7 @@ def decrypt(self, data: str): return data +@require_cryptography def pad_data(data: str): """Pad data to be block-aligned for AES encryption. @@ -262,6 +284,7 @@ def pad_data(data: str): return padder.update(str(data).encode()) + padder.finalize() +@require_cryptography def unpad_data(data: dict): """Unpad data after decryption. From f2adaf6bb1c9ecd5e19810aa78795abb106b8fdf Mon Sep 17 00:00:00 2001 From: Ryan Galloway Date: Thu, 2 Apr 2026 05:38:52 -0700 Subject: [PATCH 5/5] Fix Python 3.10 PYVERSION test regression --- tests/fixtures/env/test.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures/env/test.env b/tests/fixtures/env/test.env index fc33d7e..006667f 100644 --- a/tests/fixtures/env/test.env +++ b/tests/fixtures/env/test.env @@ -1,7 +1,7 @@ #!/usr/bin/env envstack include: [default] all: &all - PYVERSION: $(python -c "import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}')") + PYVERSION: $(python -c "import sys; print(repr(f'{sys.version_info[0]}.{sys.version_info[1]}'))") PYTHONPATH: ${DEPLOY_ROOT}/lib/python${PYVERSION} darwin: <<: *all