summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ruff.toml8
-rw-r--r--CHANGELOG.md29
-rw-r--r--README.md18
-rw-r--r--pyproject.toml54
-rw-r--r--setup.cfg45
-rw-r--r--src/etoolkit/__init__.py6
-rw-r--r--src/etoolkit/__main__.py18
-rw-r--r--src/etoolkit/etoolkit.py19
-rw-r--r--tests/conftest.py30
-rw-r--r--tests/test_cli.py11
-rw-r--r--tests/test_envtoolkit_instance.py2
-rw-r--r--tests/test_envtoolkit_instance_static.py2
12 files changed, 126 insertions, 116 deletions
diff --git a/.ruff.toml b/.ruff.toml
index add2491..769f83c 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -1,11 +1,11 @@
1cache-dir = "~/.cache/ruff" 1cache-dir = "~/.cache/ruff"
2indent-width = 4 2indent-width = 4
3line-length = 79 3line-length = 79
4target-version = "py312" 4target-version = "py310"
5 5
6[lint] 6[lint]
7select = ["ALL"] 7select = ["ALL"]
8ignore = ["ANN", "COM812", "D105", "D202", "D203", "D205", "D211", "D212", "D400", "D401", "D403", "D415", "ERA001", "FBT001", "FBT002", "PTH111", "RUF012", "RUF013", "S101", "TRY300", "BLE001", "UP020", "C901", "D200", "D402", "EM101", "EM102", "FBT003", "INP001", "PLR0912", "PLR0913", "PLR0915", "PLR2004", "PLW2901", "S603", "T201", "TRY003", "TRY400"] 8ignore = ["COM812", "D105", "D202", "D203", "D205", "D211", "D212", "D400", "D401", "D403", "D415", "ERA001", "FBT001", "FBT002", "PTH111", "RUF012", "RUF013", "S101", "TRY300", "BLE001", "UP020", "C901", "D200", "D402", "EM101", "EM102", "FBT003", "INP001", "PLR0912", "PLR0913", "PLR0915", "PLR2004", "PLW2901", "S603", "T201", "TRY003", "TRY400"]
9# D105 - Missing docstring in magic method 9# D105 - Missing docstring in magic method
10# D200 - One-line docstring should fit on one line 10# D200 - One-line docstring should fit on one line
11# D203 - 1 blank line required before class docstring 11# D203 - 1 blank line required before class docstring
@@ -42,6 +42,10 @@ ignore = ["ANN", "COM812", "D105", "D202", "D203", "D205", "D211", "D212", "D400
42fixable = ["ALL"] 42fixable = ["ALL"]
43unfixable = [] 43unfixable = []
44 44
45[lint.per-file-ignores]
46"tests/*.py" = ["ANN"] # Do not require annotations for tests
47
48
45[format] 49[format]
46# Like Black, use double quotes for strings. 50# Like Black, use double quotes for strings.
47quote-style = "single" 51quote-style = "single"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 916a9a2..fe4373f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,17 +1,28 @@
1# Changelog 1# Changelog
2 2
3## [2.1.0](https://github.com/blackm0re/etoolkit/tree/2.1.0) (2024-12-26) 3## [2.2.0](https://codeberg.org/sgs/etoolkit/releases/tag/2.2.0) (2026-04-16)
4 4
5[Full Changelog](https://github.com/blackm0re/etoolkit/compare/2.0.0...2.1.0) 5[Full Changelog](https://codeberg.org/sgs/etoolkit/compare/2.1.0...2.2.0)
6
7**Changes:**
8
9- new *%e* macro - current env. var. value (for the same key)
10
11- migrated the project from GitHub to Codeberg
12
13
14## [2.1.0](https://codeberg.org/sgs/etoolkit/releases/tag/2.1.0) (2024-12-26)
15
16[Full Changelog](https://codeberg.org/sgs/etoolkit/compare/2.0.0...2.1.0)
6 17
7**Changes:** 18**Changes:**
8 19
9- add encrypted values to *ETOOLKIT_SENSITIVE* implicitly 20- add encrypted values to *ETOOLKIT_SENSITIVE* implicitly
10 21
11 22
12## [2.0.0](https://github.com/blackm0re/etoolkit/tree/2.0.0) (2024-05-13) 23## [2.0.0](https://codeberg.org/sgs/etoolkit/releases/tag/2.0.0) (2024-05-13)
13 24
14[Full Changelog](https://github.com/blackm0re/etoolkit/compare/1.2.0...2.0.0) 25[Full Changelog](https://codeberg.org/sgs/etoolkit/compare/1.2.0...2.0.0)
15 26
16**Changes:** 27**Changes:**
17 28
@@ -24,9 +35,9 @@
24- new etoolkit.EtoolkitInstance API (not compatible with v1) 35- new etoolkit.EtoolkitInstance API (not compatible with v1)
25 36
26 37
27## [1.2.0](https://github.com/blackm0re/etoolkit/tree/1.2.0) (2022-04-04) 38## [1.2.0](https://codeberg.org/sgs/etoolkit/releases/tag/1.2.0) (2022-04-04)
28 39
29[Full Changelog](https://github.com/blackm0re/etoolkit/compare/1.1.0...1.2.0) 40[Full Changelog](https://codeberg.org/sgs/etoolkit/compare/1.1.0...1.2.0)
30 41
31**Changes:** 42**Changes:**
32 43
@@ -35,9 +46,9 @@
35- new *%p* macro - parent value (for the same key) 46- new *%p* macro - parent value (for the same key)
36 47
37 48
38## [1.1.0](https://github.com/blackm0re/etoolkit/tree/1.1.0) (2022-03-14) 49## [1.1.0](https://codeberg.org/sgs/etoolkit/releases/tag/1.1.0) (2022-03-14)
39 50
40[Full Changelog](https://github.com/blackm0re/etoolkit/compare/1.0.0...1.1.0) 51[Full Changelog](https://codeberg.org/sgs/etoolkit/compare/1.0.0...1.1.0)
41 52
42**Changes:** 53**Changes:**
43 54
@@ -50,7 +61,7 @@
50- use *setuptools* instead of the deprecated *distutils* 61- use *setuptools* instead of the deprecated *distutils*
51 62
52 63
53# [1.0.0](https://github.com/blackm0re/etoolkit/tree/1.0.0) (2021-12-23) 64# [1.0.0](https://codeberg.org/sgs/etoolkit/releases/tag/1.0.0) (2021-12-23)
54 65
55**Changes:** 66**Changes:**
56 67
diff --git a/README.md b/README.md
index b4f7efe..bdd97e3 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ goals were set:
21 21
22## Requirements 22## Requirements
23 23
24Apart from Python >= 3.8, the only requirement is 24Apart from Python >= 3.10, the only requirement is
25[cryptography](https://pypi.org/project/cryptography/) 25[cryptography](https://pypi.org/project/cryptography/)
26 26
27 27
@@ -183,7 +183,7 @@ Most users (including me) will simply use the command line interface (CLI).
183 183
184The *etoolkit* CLI loads the configuration located by default in 184The *etoolkit* CLI loads the configuration located by default in
185*~/.etoolkit.json*. That file is based on 185*~/.etoolkit.json*. That file is based on
186[etoolkit_sample.json](https://github.com/blackm0re/etoolkit/blob/master/etoolkit_sample.json). 186[etoolkit_sample.json](https://codeberg.org/sgs/etoolkit/src/branch/master/etoolkit_sample.json).
187 187
188The *"instances"* part of the configuration defines unique instances, each of 188The *"instances"* part of the configuration defines unique instances, each of
189which represents its own environment with its own defined env. variables. 189which represents its own environment with its own defined env. variables.
@@ -262,6 +262,8 @@ When all values are fetched from a given instance (and its parents) and then
262decrypted, they are further processed by replacing macros with their 262decrypted, they are further processed by replacing macros with their
263corresponding values. Currently the following macros are supported: 263corresponding values. Currently the following macros are supported:
264 264
265- **%e** - the current value of the env. var. corresponding to the same key
266
265- **%h** - the home directory of the user running *etoolkit* (~/) 267- **%h** - the home directory of the user running *etoolkit* (~/)
266 268
267- **%i** - the name of the instance that is about to be loaded 269- **%i** - the name of the instance that is about to be loaded
@@ -395,18 +397,18 @@ bottom of your bash startup file:
395 ``` 397 ```
396 398
397A complete bash completion script for *etoolkit* can be found here: 399A complete bash completion script for *etoolkit* can be found here:
398[https://github.com/blackm0re/etoolkit/blob/master/completion/etoolkit.bash](https://github.com/blackm0re/etoolkit/blob/master/completion/etoolkit.bash) 400[https://codeberg.org/sgs/etoolkit/src/branch/master/completion/etoolkit.bash](https://codeberg.org/sgs/etoolkit/src/branch/master/completion/etoolkit.bash)
399 401
400 402
401## Changelog 403## Changelog
402 404
403A complete changelog can be found at: 405A complete changelog can be found at:
404[https://github.com/blackm0re/etoolkit/blob/master/CHANGELOG.md](https://github.com/blackm0re/etoolkit/blob/master/CHANGELOG.md) 406[https://codeberg.org/sgs/etoolkit/src/branch/master/CHANGELOG.md](https://codeberg.org/sgs/etoolkit/src/branch/master/CHANGELOG.md)
405 407
406 408
407## Support and contributing 409## Support and contributing
408 410
409*etoolkit* is hosted on GitHub: https://github.com/blackm0re/etoolkit 411*etoolkit* is hosted on Codeberg: https://codeberg.org/sgs/etoolkit
410 412
411 413
412## Author 414## Author
@@ -414,11 +416,11 @@ A complete changelog can be found at:
414Simeon Simeonov - sgs @ LiberaChat 416Simeon Simeonov - sgs @ LiberaChat
415 417
416 418
417## [License](https://github.com/blackm0re/etoolkit/blob/master/LICENSE) 419## [License](https://codeberg.org/sgs/etoolkit/src/branch/master/LICENSE)
418 420
419Copyright (C) 2021-2024 Simeon Simeonov 421Copyright (C) 2021-2026 Simeon Simeonov
420All rights reserved. 422All rights reserved.
421 423
422[Licensed](https://github.com/blackm0re/etoolkit/blob/master/LICENSE) under the 424[Licensed](https://codeberg.org/sgs/etoolkit/src/branch/master/LICENSE) under the
423GNU General Public License v3.0 or later. 425GNU General Public License v3.0 or later.
424SPDX-License-Identifier: GPL-3.0-or-later 426SPDX-License-Identifier: GPL-3.0-or-later
diff --git a/pyproject.toml b/pyproject.toml
index 86a1527..e44adbb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,10 +1,42 @@
1[build-system] 1[project]
2requires = [ 2name = "etoolkit"
3 "setuptools>=51", 3description = "A simple toolkit for setting environment variables in a flexible way"
4 "wheel" 4dynamic = ["version"]
5license = "GPL-3.0-or-later"
6authors = [{name = "Simeon Simeonov", email = "sgs@pichove.org"}]
7readme = "README.md"
8requires-python = ">= 3.10"
9
10classifiers = [
11 "Development Status :: 5 - Production/Stable",
12 "Environment :: Console",
13 "Intended Audience :: Developers",
14 "Intended Audience :: System Administrators",
15 "Programming Language :: Python :: 3",
16 "Programming Language :: Python :: 3.10",
17 "Programming Language :: Python :: 3.11",
18 "Programming Language :: Python :: 3.12",
19 "Programming Language :: Python :: 3.13",
20 "Programming Language :: Python :: 3.14",
21 "Programming Language :: Python :: 3.15",
22 "Operating System :: POSIX",
23 "Topic :: Security :: Cryptography",
5] 24]
6 25
7build-backend = "setuptools.build_meta" 26dependencies = [
27 "cryptography>=3.2"
28]
29
30
31[project.scripts]
32etoolkit = "etoolkit.__main__:main"
33
34
35[project.urls]
36Homepage = "https://codeberg.org/sgs/etoolkit"
37Repository = "https://codeberg.org/sgs/etoolkit"
38Issues = "https://codeberg.org/sgs/etoolkit/issues"
39Changelog = "https://codeberg.org/sgs/etoolkit/src/branch/master/CHANGELOG.md"
8 40
9 41
10[tool.pytest.ini_options] 42[tool.pytest.ini_options]
@@ -16,3 +48,15 @@ testpaths = [
16pythonpath = [ 48pythonpath = [
17 "src" 49 "src"
18] 50]
51
52
53[tool.setuptools.dynamic]
54version = {attr = "etoolkit.__version__"}
55
56
57[build-system]
58requires = [
59 "setuptools >= 77.0.3",
60 "wheel"
61]
62build-backend = "setuptools.build_meta"
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 1838e18..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,45 +0,0 @@
1[metadata]
2name = etoolkit
3version = attr: etoolkit.__version__
4author = attr: etoolkit.__author__
5author_email = sgs@pichove.org
6description = A simple toolkit for setting environment variables in a flexible way
7long_description = file: README.md
8long_description_content_type = text/markdown
9url = https://github.com/blackm0re/etoolkit
10
11classifiers =
12 Development Status :: 5 - Production/Stable
13 Environment :: Console
14 Intended Audience :: Developers
15 Intended Audience :: System Administrators
16 License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
17 Programming Language :: Python :: 3
18 Programming Language :: Python :: 3.9
19 Programming Language :: Python :: 3.10
20 Programming Language :: Python :: 3.11
21 Programming Language :: Python :: 3.12
22 Programming Language :: Python :: 3.13
23 Programming Language :: Python :: 3.14
24 Operating System :: POSIX
25 Topic :: Security :: Cryptography
26
27project_urls =
28 Bug Tracker = https://github.com/blackm0re/etoolkit/issues
29 Source = https://github.com/blackm0re/etoolkit
30
31[options]
32package_dir =
33 = src
34packages = find:
35python_requires = >=3.9
36
37install_requires =
38 cryptography>=3.2
39
40[options.packages.find]
41where = src
42
43[options.entry_points]
44console_scripts =
45 etoolkit = etoolkit.__main__:main
diff --git a/src/etoolkit/__init__.py b/src/etoolkit/__init__.py
index 4cf5d6e..fc32b61 100644
--- a/src/etoolkit/__init__.py
+++ b/src/etoolkit/__init__.py
@@ -1,5 +1,5 @@
1# etoolkit 1# etoolkit
2# Copyright (C) 2021-2024 Simeon Simeonov 2# Copyright (C) 2021-2026 Simeon Simeonov
3 3
4# This program is free software: you can redistribute it and/or modify 4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by 5# it under the terms of the GNU General Public License as published by
@@ -18,11 +18,11 @@
18from .etoolkit import EtoolkitInstance, EtoolkitInstanceError 18from .etoolkit import EtoolkitInstance, EtoolkitInstanceError
19 19
20__author__ = 'Simeon Simeonov' 20__author__ = 'Simeon Simeonov'
21__version__ = '2.1.0' 21__version__ = '2.2.0'
22__license__ = 'GPL3' 22__license__ = 'GPL3'
23 23
24 24
25def int_or_str(value): 25def int_or_str(value: int | str) -> int | str:
26 """Returns int value of value when possible""" 26 """Returns int value of value when possible"""
27 try: 27 try:
28 return int(value) 28 return int(value)
diff --git a/src/etoolkit/__main__.py b/src/etoolkit/__main__.py
index a2f2f20..cef3d42 100644
--- a/src/etoolkit/__main__.py
+++ b/src/etoolkit/__main__.py
@@ -1,5 +1,5 @@
1# etoolkit 1# etoolkit
2# Copyright (C) 2021-2024 Simeon Simeonov 2# Copyright (C) 2021-2026 Simeon Simeonov
3 3
4# This program is free software: you can redistribute it and/or modify 4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by 5# it under the terms of the GNU General Public License as published by
@@ -50,7 +50,7 @@ class EtoolkitCLIHandler:
50 a part of the etoolkit API 50 a part of the etoolkit API
51 """ 51 """
52 52
53 def __init__(self, args: argparse.Namespace, config_dict: dict): 53 def __init__(self, args: argparse.Namespace, config_dict: dict) -> None:
54 """ 54 """
55 :param args: The parsed argparse arguments sent by the caller 55 :param args: The parsed argparse arguments sent by the caller
56 :type args: argparse.Namespace 56 :type args: argparse.Namespace
@@ -69,7 +69,7 @@ class EtoolkitCLIHandler:
69 69
70 self._password_from_env = os.environ.get('ETOOLKIT_MASTER_PASSWORD') 70 self._password_from_env = os.environ.get('ETOOLKIT_MASTER_PASSWORD')
71 71
72 def decrypt_value(self): 72 def decrypt_value(self) -> None:
73 """ 73 """
74 Interactive method for decrypting value(s) 74 Interactive method for decrypting value(s)
75 75
@@ -110,7 +110,7 @@ class EtoolkitCLIHandler:
110 break 110 break
111 return 111 return
112 112
113 def encrypt_value(self): 113 def encrypt_value(self) -> None:
114 """ 114 """
115 Interactive method for encrypting value(s) 115 Interactive method for encrypting value(s)
116 116
@@ -155,7 +155,7 @@ class EtoolkitCLIHandler:
155 break 155 break
156 return 156 return
157 157
158 def generate_master_password_hash(self): 158 def generate_master_password_hash(self) -> None:
159 """ 159 """
160 Interactive method for generating password hash 160 Interactive method for generating password hash
161 161
@@ -168,7 +168,7 @@ class EtoolkitCLIHandler:
168 ) 168 )
169 print(f'Master password hash: {phash}') 169 print(f'Master password hash: {phash}')
170 170
171 def list(self): 171 def list(self) -> None:
172 """Lists all instances defined in the config file""" 172 """Lists all instances defined in the config file"""
173 173
174 for instance_name in sorted( 174 for instance_name in sorted(
@@ -179,7 +179,7 @@ class EtoolkitCLIHandler:
179 ): 179 ):
180 print(instance_name) 180 print(instance_name)
181 181
182 def load_instance(self): 182 def load_instance(self) -> None:
183 """Loads a single specified instance from the config file""" 183 """Loads a single specified instance from the config file"""
184 184
185 inst = etoolkit.EtoolkitInstance( 185 inst = etoolkit.EtoolkitInstance(
@@ -208,7 +208,7 @@ class EtoolkitCLIHandler:
208 os.environ.get('SHELL', 'bash').split(), check=False 208 os.environ.get('SHELL', 'bash').split(), check=False
209 ) 209 )
210 210
211 def reencrypt(self): 211 def reencrypt(self) -> None:
212 """ 212 """
213 Interactive method that prints new configuration data (JSON) to stdout 213 Interactive method that prints new configuration data (JSON) to stdout
214 214
@@ -277,7 +277,7 @@ class EtoolkitCLIHandler:
277 ) 277 )
278 278
279 279
280def main(inargs=None): 280def main(inargs: list = None) -> None:
281 """main entry point""" 281 """main entry point"""
282 282
283 parser = argparse.ArgumentParser( 283 parser = argparse.ArgumentParser(
diff --git a/src/etoolkit/etoolkit.py b/src/etoolkit/etoolkit.py
index a933a01..f5ce2a6 100644
--- a/src/etoolkit/etoolkit.py
+++ b/src/etoolkit/etoolkit.py
@@ -1,5 +1,5 @@
1# etoolkit 1# etoolkit
2# Copyright (C) 2021-2024 Simeon Simeonov 2# Copyright (C) 2021-2026 Simeon Simeonov
3 3
4# This program is free software: you can redistribute it and/or modify 4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by 5# it under the terms of the GNU General Public License as published by
@@ -19,6 +19,7 @@ import base64
19import getpass 19import getpass
20import hashlib 20import hashlib
21import os 21import os
22from collections.abc import Callable
22 23
23from cryptography.exceptions import InvalidTag 24from cryptography.exceptions import InvalidTag
24from cryptography.hazmat.primitives.ciphers.aead import AESGCM 25from cryptography.hazmat.primitives.ciphers.aead import AESGCM
@@ -33,7 +34,7 @@ class EtoolkitInstanceError(Exception):
33class EtoolkitInstance: 34class EtoolkitInstance:
34 """A basic class representing a single instance""" 35 """A basic class representing a single instance"""
35 36
36 def __init__(self, name: str, data: dict): 37 def __init__(self, name: str, data: dict) -> None:
37 """ 38 """
38 :param name: Instance name 39 :param name: Instance name
39 :type name: str 40 :type name: str
@@ -76,14 +77,14 @@ class EtoolkitInstance:
76 ] 77 ]
77 78
78 @property 79 @property
79 def master_password(self) -> str: 80 def master_password(self) -> str | None:
80 """master_password-property""" 81 """master_password-property"""
81 if self._master_password is not None: 82 if self._master_password is not None:
82 return self._master_password 83 return self._master_password
83 return None if self._parent is None else self._parent.master_password 84 return None if self._parent is None else self._parent.master_password
84 85
85 @master_password.setter 86 @master_password.setter
86 def master_password(self, value): 87 def master_password(self, value: str) -> None:
87 """master_password-property setter""" 88 """master_password-property setter"""
88 self._master_password = value 89 self._master_password = value
89 if self._parent is not None and self._parent.master_password is None: 90 if self._parent is not None and self._parent.master_password is None:
@@ -95,7 +96,7 @@ class EtoolkitInstance:
95 return self._master_password_hash 96 return self._master_password_hash
96 97
97 @master_password_hash.setter 98 @master_password_hash.setter
98 def master_password_hash(self, value): 99 def master_password_hash(self, value: str) -> None:
99 """master_password_hash-property setter""" 100 """master_password_hash-property setter"""
100 self._master_password_hash = value 101 self._master_password_hash = value
101 102
@@ -105,12 +106,12 @@ class EtoolkitInstance:
105 return self._name 106 return self._name
106 107
107 @property 108 @property
108 def prompt_func(self): 109 def prompt_func(self) -> Callable[[str, bool], str]:
109 """prompt_func-property""" 110 """prompt_func-property"""
110 return self._prompt_func 111 return self._prompt_func
111 112
112 @prompt_func.setter 113 @prompt_func.setter
113 def prompt_func(self, value): 114 def prompt_func(self, value: Callable[[str, bool], str]) -> None:
114 """prompt_func-property setter""" 115 """prompt_func-property setter"""
115 self._prompt_func = value 116 self._prompt_func = value
116 if self._parent is not None and self._parent.prompt_func is None: 117 if self._parent is not None and self._parent.prompt_func is None:
@@ -294,7 +295,7 @@ class EtoolkitInstance:
294 ) 295 )
295 296
296 @staticmethod 297 @staticmethod
297 def parse_value(value, macros: dict): 298 def parse_value(value: object, macros: dict) -> object:
298 """ 299 """
299 Returns the value with all macros replaced by their values 300 Returns the value with all macros replaced by their values
300 301
@@ -412,6 +413,8 @@ class EtoolkitInstance:
412 if not value: 413 if not value:
413 # perhaps unset instead of skipping? 414 # perhaps unset instead of skipping?
414 continue 415 continue
416 if isinstance(value, str) and '%e' in value:
417 macros['%e'] = os.environ.get(key, '')
415 if isinstance(value, str) and '%p' in value: 418 if isinstance(value, str) and '%p' in value:
416 macros['%p'] = ( 419 macros['%p'] = (
417 self._parent.get_environ().get(key, '') 420 self._parent.get_environ().get(key, '')
diff --git a/tests/conftest.py b/tests/conftest.py
index a4463fd..6cc8b9f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,5 +1,5 @@
1# etoolkit 1# etoolkit
2# Copyright (C) 2021-2024 Simeon Simeonov 2# Copyright (C) 2021-2026 Simeon Simeonov
3 3
4# This program is free software: you can redistribute it and/or modify 4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by 5# it under the terms of the GNU General Public License as published by
@@ -20,7 +20,7 @@ import json
20import pytest 20import pytest
21 21
22 22
23@pytest.fixture() 23@pytest.fixture
24def config_data(): 24def config_data():
25 """config_data for testing""" 25 """config_data for testing"""
26 26
@@ -59,7 +59,7 @@ def config_data():
59 } 59 }
60 60
61 61
62@pytest.fixture() 62@pytest.fixture
63def config_file(tmp_path, config_data): 63def config_file(tmp_path, config_data):
64 """Temporary config file for testing that includes config_data""" 64 """Temporary config file for testing that includes config_data"""
65 65
@@ -68,7 +68,7 @@ def config_file(tmp_path, config_data):
68 return str(cf) 68 return str(cf)
69 69
70 70
71@pytest.fixture() 71@pytest.fixture
72def long_encrypted_value(): 72def long_encrypted_value():
73 """enc. value corresponding to 'Nobody expects the Spanish inquisition'""" 73 """enc. value corresponding to 'Nobody expects the Spanish inquisition'"""
74 74
@@ -79,25 +79,25 @@ def long_encrypted_value():
79 ) 79 )
80 80
81 81
82@pytest.fixture() 82@pytest.fixture
83def long_value(): 83def long_value():
84 """standard value (> 32 bytes)""" 84 """standard value (> 32 bytes)"""
85 return 'Nobody expects the Spanish inquisition' 85 return 'Nobody expects the Spanish inquisition'
86 86
87 87
88@pytest.fixture() 88@pytest.fixture
89def master_password(): 89def master_password():
90 """Master passord""" 90 """Master passord"""
91 return 'The very secret passwd' 91 return 'The very secret passwd'
92 92
93 93
94@pytest.fixture() 94@pytest.fixture
95def new_master_password(): 95def new_master_password():
96 """Master passord""" 96 """Master passord"""
97 return 'New very secret passwd' 97 return 'New very secret passwd'
98 98
99 99
100@pytest.fixture() 100@pytest.fixture
101def non_random_bytes_32(): 101def non_random_bytes_32():
102 """always use the same bytes instead of os.urandom(32)""" 102 """always use the same bytes instead of os.urandom(32)"""
103 103
@@ -107,7 +107,7 @@ def non_random_bytes_32():
107 ) 107 )
108 108
109 109
110@pytest.fixture() 110@pytest.fixture
111def non_random_bytes_57(): 111def non_random_bytes_57():
112 """always use the same bytes instead of os.urandom(57)""" 112 """always use the same bytes instead of os.urandom(57)"""
113 113
@@ -118,14 +118,14 @@ def non_random_bytes_57():
118 ) 118 )
119 119
120 120
121@pytest.fixture() 121@pytest.fixture
122def nonexistent_config_file(tmp_path): 122def nonexistent_config_file(tmp_path):
123 """temporary config file for testing that includes config_data""" 123 """temporary config file for testing that includes config_data"""
124 124
125 return str(tmp_path / 'etoolkitt.json') 125 return str(tmp_path / 'etoolkitt.json')
126 126
127 127
128@pytest.fixture() 128@pytest.fixture
129def password_hash(): 129def password_hash():
130 """password hash for testing, corresponding to 'The very secret passwd'""" 130 """password hash for testing, corresponding to 'The very secret passwd'"""
131 131
@@ -135,7 +135,7 @@ def password_hash():
135 ) 135 )
136 136
137 137
138@pytest.fixture() 138@pytest.fixture
139def short_encrypted_value(): 139def short_encrypted_value():
140 """enc. value corresponding to 'secret1'""" 140 """enc. value corresponding to 'secret1'"""
141 141
@@ -145,7 +145,7 @@ def short_encrypted_value():
145 ) 145 )
146 146
147 147
148@pytest.fixture() 148@pytest.fixture
149def short_encrypted_value_v1(): 149def short_encrypted_value_v1():
150 """enc. value (enc-val 1) corresponding to 'secret1'""" 150 """enc. value (enc-val 1) corresponding to 'secret1'"""
151 151
@@ -155,13 +155,13 @@ def short_encrypted_value_v1():
155 ) 155 )
156 156
157 157
158@pytest.fixture() 158@pytest.fixture
159def short_value(): 159def short_value():
160 """standard value (< 32 bytes)""" 160 """standard value (< 32 bytes)"""
161 return 'secret1' 161 return 'secret1'
162 162
163 163
164@pytest.fixture() 164@pytest.fixture
165def wrong_master_password(): 165def wrong_master_password():
166 """Wrong master passord""" 166 """Wrong master passord"""
167 return 'the very secret passwd' 167 return 'the very secret passwd'
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 953abf4..b7913da 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,5 +1,5 @@
1# etoolkit 1# etoolkit
2# Copyright (C) 2021-2024 Simeon Simeonov 2# Copyright (C) 2021-2026 Simeon Simeonov
3 3
4# This program is free software: you can redistribute it and/or modify 4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by 5# it under the terms of the GNU General Public License as published by
@@ -38,7 +38,6 @@ def test_decrypt_v1(binput, capsys, config_file, master_password):
38 ): 38 ):
39 with pytest.raises(SystemExit) as exit_info: 39 with pytest.raises(SystemExit) as exit_info:
40 main(['-c', f'{config_file}', '-d']) 40 main(['-c', f'{config_file}', '-d'])
41 assert exit_info.type == SystemExit
42 assert exit_info.value.code == 0 41 assert exit_info.value.code == 0
43 assert capsys.readouterr().out.strip() == 'Decrypted value: bar' 42 assert capsys.readouterr().out.strip() == 'Decrypted value: bar'
44 43
@@ -60,7 +59,6 @@ def test_decrypt_v2(
60 ): 59 ):
61 with pytest.raises(SystemExit) as exit_info: 60 with pytest.raises(SystemExit) as exit_info:
62 main(['-c', f'{config_file}', '-d']) 61 main(['-c', f'{config_file}', '-d'])
63 assert exit_info.type == SystemExit
64 assert exit_info.value.code == 0 62 assert exit_info.value.code == 0
65 assert capsys.readouterr().out.strip() == ( 63 assert capsys.readouterr().out.strip() == (
66 f'Decrypted value: {short_value}' 64 f'Decrypted value: {short_value}'
@@ -88,7 +86,6 @@ def test_encrypt_with_echo(
88 ): 86 ):
89 with pytest.raises(SystemExit) as exit_info: 87 with pytest.raises(SystemExit) as exit_info:
90 main(['-c', f'{config_file}', '-e', '-E']) 88 main(['-c', f'{config_file}', '-e', '-E'])
91 assert exit_info.type == SystemExit
92 assert exit_info.value.code == 0 89 assert exit_info.value.code == 0
93 assert capsys.readouterr().out.strip() == ( 90 assert capsys.readouterr().out.strip() == (
94 f'Encrypted value: {short_encrypted_value}' 91 f'Encrypted value: {short_encrypted_value}'
@@ -116,7 +113,6 @@ def test_encrypt_without_echo(
116 ): 113 ):
117 with pytest.raises(SystemExit) as exit_info: 114 with pytest.raises(SystemExit) as exit_info:
118 main(['-c', f'{config_file}', '-e']) 115 main(['-c', f'{config_file}', '-e'])
119 assert exit_info.type == SystemExit
120 assert exit_info.value.code == 0 116 assert exit_info.value.code == 0
121 assert capsys.readouterr().out.strip() == ( 117 assert capsys.readouterr().out.strip() == (
122 f'Encrypted value: {short_encrypted_value}' 118 f'Encrypted value: {short_encrypted_value}'
@@ -139,11 +135,9 @@ def test_list(capsys, config_file, nonexistent_config_file):
139 135
140 with pytest.raises(SystemExit) as exit_info: 136 with pytest.raises(SystemExit) as exit_info:
141 main(['-c', nonexistent_config_file, '-l']) 137 main(['-c', nonexistent_config_file, '-l'])
142 assert exit_info.type == SystemExit
143 assert exit_info.value.code == errno.EIO 138 assert exit_info.value.code == errno.EIO
144 with pytest.raises(SystemExit) as exit_info: 139 with pytest.raises(SystemExit) as exit_info:
145 main(['-c', config_file, '-l']) 140 main(['-c', config_file, '-l'])
146 assert exit_info.type == SystemExit
147 assert exit_info.value.code == 0 141 assert exit_info.value.code == 0
148 assert capsys.readouterr().out.strip() == f'dev{os.linesep}secret' 142 assert capsys.readouterr().out.strip() == f'dev{os.linesep}secret'
149 143
@@ -153,7 +147,6 @@ def test_help(capsys):
153 147
154 with pytest.raises(SystemExit) as exit_info: 148 with pytest.raises(SystemExit) as exit_info:
155 main(['-h']) 149 main(['-h'])
156 assert exit_info.type == SystemExit
157 assert exit_info.value.code == 0 150 assert exit_info.value.code == 0
158 assert capsys.readouterr().out.startswith('usage: etoolkit') 151 assert capsys.readouterr().out.startswith('usage: etoolkit')
159 152
@@ -169,7 +162,6 @@ def test_generate_master_password_hash(
169 urandom.return_value = non_random_bytes_32 162 urandom.return_value = non_random_bytes_32
170 with pytest.raises(SystemExit) as exit_info: 163 with pytest.raises(SystemExit) as exit_info:
171 main(['--generate-master-password-hash']) 164 main(['--generate-master-password-hash'])
172 assert exit_info.type == SystemExit
173 assert exit_info.value.code == 0 165 assert exit_info.value.code == 0
174 assert capsys.readouterr().out.strip() == ( 166 assert capsys.readouterr().out.strip() == (
175 'Master password hash: pbkdf2_sha256$500000$uYpZM1VfAGq0CDZL2duITs076' 167 'Master password hash: pbkdf2_sha256$500000$uYpZM1VfAGq0CDZL2duITs076'
@@ -182,7 +174,6 @@ def test_version(capsys):
182 174
183 with pytest.raises(SystemExit) as exit_info: 175 with pytest.raises(SystemExit) as exit_info:
184 main(['-v']) 176 main(['-v'])
185 assert exit_info.type == SystemExit
186 assert exit_info.value.code == 0 177 assert exit_info.value.code == 0
187 assert capsys.readouterr().out.startswith( 178 assert capsys.readouterr().out.startswith(
188 f'etoolkit {etoolkit.__version__}' 179 f'etoolkit {etoolkit.__version__}'
diff --git a/tests/test_envtoolkit_instance.py b/tests/test_envtoolkit_instance.py
index f555f81..5137e9f 100644
--- a/tests/test_envtoolkit_instance.py
+++ b/tests/test_envtoolkit_instance.py
@@ -1,5 +1,5 @@
1# etoolkit 1# etoolkit
2# Copyright (C) 2021-2024 Simeon Simeonov 2# Copyright (C) 2021-2026 Simeon Simeonov
3 3
4# This program is free software: you can redistribute it and/or modify 4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by 5# it under the terms of the GNU General Public License as published by
diff --git a/tests/test_envtoolkit_instance_static.py b/tests/test_envtoolkit_instance_static.py
index ee5cf9e..e13d439 100644
--- a/tests/test_envtoolkit_instance_static.py
+++ b/tests/test_envtoolkit_instance_static.py
@@ -1,5 +1,5 @@
1# etoolkit 1# etoolkit
2# Copyright (C) 2021-2024 Simeon Simeonov 2# Copyright (C) 2021-2026 Simeon Simeonov
3 3
4# This program is free software: you can redistribute it and/or modify 4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by 5# it under the terms of the GNU General Public License as published by