Skip to content

Commit 06ca54d

Browse files
authored
Version 1.0.2 (#114)
* Move return outside of finally block * Harden crypto fallbacks and expand local/CI test coverage * Fix Python 3.10 PYVERSION test regression
1 parent db25987 commit 06ca54d

7 files changed

Lines changed: 56 additions & 15 deletions

File tree

.github/workflows/tests.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ jobs:
88
strategy:
99
matrix:
1010
os: [ubuntu-latest, macos-latest, windows-latest]
11+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
1112
steps:
1213
- uses: actions/checkout@v4
1314
- uses: actions/setup-python@v5
1415
with:
15-
python-version: "3.11"
16+
python-version: ${{ matrix.python-version }}
1617
- run: python -m pip install --upgrade pip
1718
- run: pip install pytest
1819
- run: pip install -e .

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
---
99

10+
## [1.0.2] - 2026-04-01
11+
12+
### Changed
13+
- add a self-contained `make test` target that installs `pytest` and the package in editable mode before running the test suite
14+
- mark `all` as a phony Make target to avoid filename collisions
15+
16+
### Fixed
17+
- remove `return` statements from `finally` blocks in encryption helpers to avoid newer Python `SyntaxWarning`s
18+
- harden optional `cryptography` imports with a decorator-based availability guard so missing crypto dependencies raise a clear runtime error instead of `NoneType` failures
19+
20+
---
21+
1022
## [1.0.1] - 2026-02-08
1123

1224
### Fixed

Makefile

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ build: clean
3636
# Combined target to build for both platforms
3737
all: build
3838

39-
# Test target to verify the build
39+
# Run the pytest suite from an editable install, matching CI behavior
4040
test:
41-
$(ENVSTACK_CMD) -- ls -al
42-
${ENVSTACK_CMD} -- which python
41+
python -m pip install pytest
42+
python -m pip install -e .
43+
pytest tests -q
4344

4445
# Install dryrun target to simulate installation
4546
dryrun:
@@ -51,4 +52,4 @@ install: build
5152
dist --force --yes
5253

5354
# Phony targets
54-
.PHONY: build dryrun install clean
55+
.PHONY: all build dryrun install clean test pytest

lib/envstack/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"""
3535

3636
__prog__ = "envstack"
37-
__version__ = "1.0.1"
37+
__version__ = "1.0.2"
3838

3939
from envstack.env import clear, init, revert, save # noqa: F401
4040
from envstack.env import load_environ, resolve_environ # noqa: F401

lib/envstack/encrypt.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,42 @@
3838
import os
3939
import secrets
4040
from base64 import b64decode, b64encode
41+
from functools import wraps
4142

4243
from envstack.logger import log
4344

4445
# cryptography and _rust dependency may not be available everywhere
4546
# ImportError: DLL load failed while importing _rust: Module not found.
47+
CRYPTOGRAPHY_AVAILABLE = False
4648
Fernet = None
49+
InvalidToken = type("InvalidToken", (Exception,), {})
50+
InvalidTag = type("InvalidTag", (Exception,), {})
51+
padding = None
52+
Cipher = None
53+
algorithms = None
54+
modes = None
4755
try:
48-
import cryptography.exceptions
4956
from cryptography.fernet import Fernet, InvalidToken
57+
from cryptography.exceptions import InvalidTag
5058
from cryptography.hazmat.primitives import padding
5159
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
60+
CRYPTOGRAPHY_AVAILABLE = True
5261
except ImportError as err:
5362
log.debug("cryptography module not available: %s", err)
5463

5564

65+
def require_cryptography(func):
66+
"""Guard crypto-backed functions when cryptography is unavailable."""
67+
68+
@wraps(func)
69+
def wrapper(*args, **kwargs):
70+
if not CRYPTOGRAPHY_AVAILABLE:
71+
raise RuntimeError("cryptography support is not available")
72+
return func(*args, **kwargs)
73+
74+
return wrapper
75+
76+
5677
class Base64Encryptor(object):
5778
"""Encrypt and decrypt secrets using base64 encoding."""
5879

@@ -84,6 +105,7 @@ def __init__(self, key: str = None, env: dict = os.environ):
84105
self.key = self.get_key(env)
85106

86107
@classmethod
108+
@require_cryptography
87109
def generate_key(csl):
88110
"""Generate a new 256-bit encryption key."""
89111
if Fernet:
@@ -104,6 +126,7 @@ def get_key(self, env: dict = os.environ):
104126
return Fernet(key)
105127
return key
106128

129+
@require_cryptography
107130
def encrypt(self, data: str):
108131
"""Encrypt a secret using Fernet.
109132
@@ -121,9 +144,9 @@ def encrypt(self, data: str):
121144
log.error("invalid value: %s", e)
122145
except Exception as e:
123146
log.error("unhandled error: %s", e)
124-
finally:
125-
return results
147+
return results
126148

149+
@require_cryptography
127150
def decrypt(self, data: str):
128151
"""Decrypt a secret using Fernet.
129152
@@ -153,6 +176,7 @@ def __init__(self, key: str = None, env: dict = os.environ):
153176
self.key = self.get_key(env)
154177

155178
@classmethod
179+
@require_cryptography
156180
def generate_key(csl):
157181
"""Generate a new 256-bit encryption key."""
158182
key = secrets.token_bytes(32)
@@ -171,6 +195,7 @@ def get_key(self, env: dict = os.environ):
171195
raise ValueError("invalid base64 encoding: %s" % e)
172196
return key
173197

198+
@require_cryptography
174199
def encrypt_data(self, secret: str):
175200
"""Encrypt a secret using AES-GCM.
176201
@@ -189,6 +214,7 @@ def encrypt_data(self, secret: str):
189214
"tag": b64encode(encryptor.tag).decode(),
190215
}
191216

217+
@require_cryptography
192218
def decrypt_data(self, encrypted_data: dict):
193219
"""Decrypt a secret using AES-GCM.
194220
@@ -218,14 +244,13 @@ def encrypt(self, data: str):
218244
results = compact_store(encrypted_data)
219245
except binascii.Error as e:
220246
log.error("invalid base64 encoding: %s", e)
221-
except cryptography.exceptions.InvalidTag:
247+
except InvalidTag:
222248
log.error("invalid encryption key")
223249
except ValueError as e:
224250
log.error("invalid value: %s", e)
225251
except Exception as e:
226252
log.error("unhandled error: %s", e)
227-
finally:
228-
return results
253+
return results
229254

230255
def decrypt(self, data: str):
231256
"""Convenience function to decrypt a secret using AES-GCM.
@@ -239,7 +264,7 @@ def decrypt(self, data: str):
239264
return decrypted.decode()
240265
except binascii.Error as e:
241266
log.debug("invalid base64 encoding: %s", e)
242-
except cryptography.exceptions.InvalidTag:
267+
except InvalidTag:
243268
log.debug("invalid encryption key")
244269
except ValueError as e:
245270
log.debug("invalid value: %s", e)
@@ -248,6 +273,7 @@ def decrypt(self, data: str):
248273
return data
249274

250275

276+
@require_cryptography
251277
def pad_data(data: str):
252278
"""Pad data to be block-aligned for AES encryption.
253279
@@ -258,6 +284,7 @@ def pad_data(data: str):
258284
return padder.update(str(data).encode()) + padder.finalize()
259285

260286

287+
@require_cryptography
261288
def unpad_data(data: dict):
262289
"""Unpad data after decryption.
263290

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "envstack"
7-
version = "1.0.1"
7+
version = "1.0.2"
88
description = "Environment variable composition layer for tools and processes."
99
readme = { file = "README.md", content-type = "text/markdown" }
1010
requires-python = ">=3.6"

tests/fixtures/env/test.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env envstack
22
include: [default]
33
all: &all
4-
PYVERSION: $(python -c "import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}')")
4+
PYVERSION: $(python -c "import sys; print(repr(f'{sys.version_info[0]}.{sys.version_info[1]}'))")
55
PYTHONPATH: ${DEPLOY_ROOT}/lib/python${PYVERSION}
66
darwin:
77
<<: *all

0 commit comments

Comments
 (0)