mm-std 0.5.3__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. mm_std-0.6.0/.claude/settings.local.json +15 -0
  2. {mm_std-0.5.3 → mm_std-0.6.0}/.pre-commit-config.yaml +1 -1
  3. mm_std-0.6.0/CLAUDE.md +13 -0
  4. {mm_std-0.5.3 → mm_std-0.6.0}/PKG-INFO +1 -1
  5. {mm_std-0.5.3 → mm_std-0.6.0}/pyproject.toml +11 -11
  6. {mm_std-0.5.3 → mm_std-0.6.0}/src/mm_std/__init__.py +4 -4
  7. {mm_std-0.5.3 → mm_std-0.6.0}/src/mm_std/dict_utils.py +35 -8
  8. {mm_std-0.5.3 → mm_std-0.6.0}/src/mm_std/json_utils.py +7 -10
  9. {mm_std-0.5.3 → mm_std-0.6.0}/src/mm_std/random_utils.py +2 -2
  10. mm_std-0.6.0/src/mm_std/subprocess_utils.py +96 -0
  11. mm_std-0.6.0/tests/test_dict_utils.py +179 -0
  12. mm_std-0.6.0/tests/test_json_utils.py +205 -0
  13. {mm_std-0.5.3 → mm_std-0.6.0}/tests/test_random_utils.py +1 -1
  14. mm_std-0.6.0/tests/test_str_utils.py +207 -0
  15. {mm_std-0.5.3 → mm_std-0.6.0}/tests/test_subprocess_utils.py +36 -32
  16. mm_std-0.6.0/uv.lock +393 -0
  17. mm_std-0.5.3/dict.dic +0 -0
  18. mm_std-0.5.3/requirements.txt +0 -2
  19. mm_std-0.5.3/src/mm_std/subprocess_utils.py +0 -75
  20. mm_std-0.5.3/tests/test_dict_utils.py +0 -169
  21. mm_std-0.5.3/tests/test_json_utils.py +0 -263
  22. mm_std-0.5.3/tests/test_str_utils.py +0 -180
  23. mm_std-0.5.3/uv.lock +0 -368
  24. {mm_std-0.5.3 → mm_std-0.6.0}/.gitignore +0 -0
  25. {mm_std-0.5.3 → mm_std-0.6.0}/README.md +0 -0
  26. {mm_std-0.5.3 → mm_std-0.6.0}/justfile +0 -0
  27. {mm_std-0.5.3 → mm_std-0.6.0}/src/mm_std/date_utils.py +0 -0
  28. {mm_std-0.5.3 → mm_std-0.6.0}/src/mm_std/py.typed +0 -0
  29. {mm_std-0.5.3 → mm_std-0.6.0}/src/mm_std/str_utils.py +0 -0
  30. {mm_std-0.5.3 → mm_std-0.6.0}/tests/__init__.py +0 -0
  31. {mm_std-0.5.3 → mm_std-0.6.0}/tests/test_date_utils.py +0 -0
@@ -0,0 +1,15 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(find:*)",
5
+ "Bash(uv run pytest:*)",
6
+ "Bash(uv run ruff:*)",
7
+ "Bash(uv run:*)",
8
+ "mcp__ide__getDiagnostics",
9
+ "Read(//Users/m/.vscode/extensions/ms-python.vscode-pylance-2025.7.1/dist/typeshed-fallback/stdlib/json/**)",
10
+ "Bash(just lint)",
11
+ "Bash(python:*)"
12
+ ],
13
+ "deny": []
14
+ }
15
+ }
@@ -1,6 +1,6 @@
1
1
  repos:
2
2
  - repo: https://github.com/pre-commit/pre-commit-hooks
3
- rev: v5.0.0
3
+ rev: v6.0.0
4
4
  hooks:
5
5
  - id: trailing-whitespace
6
6
  - id: end-of-file-fixer
mm_std-0.6.0/CLAUDE.md ADDED
@@ -0,0 +1,13 @@
1
+ # Claude Guidelines
2
+
3
+ ## Critical Guidelines
4
+
5
+ 1. **Always communicate in English** - Regardless of the language the user speaks, always respond in English. All code, comments, and documentation must be in English.
6
+
7
+ 2. **Minimal documentation** - Only add comments/documentation when it simplifies understanding and isn't obvious from the code itself. Keep it strictly relevant and concise.
8
+
9
+ 3. **Critical thinking** - Always critically evaluate user ideas. Users can make mistakes. Think first about whether the user's idea is good before implementing.
10
+
11
+ 4. **Lint after changes** - After making code changes, always run `just lint` to verify code quality and fix any linter issues.
12
+
13
+ 5. **No disabling linter rules** - Never use special disabling comments (like `# noqa`, `# type: ignore`, `# ruff: noqa`, etc.) to turn off linter rules without explicit permission. If you believe a rule should be disabled, ask first.
@@ -1,4 +1,4 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-std
3
- Version: 0.5.3
3
+ Version: 0.6.0
4
4
  Requires-Python: >=3.13
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mm-std"
3
- version = "0.5.3"
3
+ version = "0.6.0"
4
4
  description = ""
5
5
  requires-python = ">=3.13"
6
6
  dependencies = [
@@ -10,25 +10,25 @@ dependencies = [
10
10
  requires = ["hatchling"]
11
11
  build-backend = "hatchling.build"
12
12
 
13
- [tool.uv]
14
- dev-dependencies = [
15
- "pytest~=8.4.0",
16
- "pytest-xdist~=3.7.0",
17
- "ruff~=0.11.13",
18
- "mypy~=1.16.0",
19
- "bandit~=1.8.3",
20
- "pre-commit~=4.2.0",
13
+ [dependency-groups]
14
+ dev = [
15
+ "pytest~=9.0.2",
16
+ "pytest-xdist~=3.8.0",
17
+ "ruff~=0.14.10",
18
+ "mypy~=1.19.1",
19
+ "bandit~=1.9.2",
20
+ "pre-commit~=4.5.1",
21
21
  ]
22
22
 
23
23
  [tool.mypy]
24
- python_version = "3.13"
24
+ python_version = "3.14"
25
25
  warn_no_return = false
26
26
  strict = true
27
27
  exclude = ["^tests/", "^tmp/"]
28
28
 
29
29
  [tool.ruff]
30
30
  line-length = 130
31
- target-version = "py313"
31
+ target-version = "py314"
32
32
  [tool.ruff.lint]
33
33
  select = ["ALL"]
34
34
  ignore = [
@@ -3,19 +3,19 @@ from .dict_utils import replace_empty_dict_entries
3
3
  from .json_utils import ExtendedJSONEncoder, json_dumps
4
4
  from .random_utils import random_datetime, random_decimal
5
5
  from .str_utils import parse_lines, str_contains_any, str_ends_with_any, str_starts_with_any
6
- from .subprocess_utils import ShellResult, shell, ssh_shell # nosec
6
+ from .subprocess_utils import CmdResult, run_cmd, run_ssh_cmd # nosec
7
7
 
8
8
  __all__ = [
9
+ "CmdResult",
9
10
  "ExtendedJSONEncoder",
10
- "ShellResult",
11
11
  "json_dumps",
12
12
  "parse_date",
13
13
  "parse_lines",
14
14
  "random_datetime",
15
15
  "random_decimal",
16
16
  "replace_empty_dict_entries",
17
- "shell",
18
- "ssh_shell",
17
+ "run_cmd",
18
+ "run_ssh_cmd",
19
19
  "str_contains_any",
20
20
  "str_ends_with_any",
21
21
  "str_starts_with_any",
@@ -1,22 +1,49 @@
1
- from collections import defaultdict
1
+ from collections import OrderedDict, defaultdict
2
2
  from collections.abc import Mapping, MutableMapping
3
3
  from decimal import Decimal
4
- from typing import TypeVar, cast
4
+ from typing import TypeVar, overload
5
5
 
6
6
  K = TypeVar("K")
7
7
  V = TypeVar("V")
8
- # TypeVar bound to MutableMapping with same K, V as defaults parameter
9
- # 'type: ignore' needed because mypy can't handle TypeVar bounds with other TypeVars
10
- DictType = TypeVar("DictType", bound=MutableMapping[K, V]) # type: ignore[valid-type]
11
8
 
12
9
 
10
+ @overload
13
11
  def replace_empty_dict_entries(
14
- data: DictType,
12
+ data: defaultdict[K, V],
15
13
  defaults: Mapping[K, V] | None = None,
16
14
  treat_zero_as_empty: bool = False,
17
15
  treat_false_as_empty: bool = False,
18
16
  treat_empty_string_as_empty: bool = True,
19
- ) -> DictType:
17
+ ) -> defaultdict[K, V]: ...
18
+
19
+
20
+ @overload
21
+ def replace_empty_dict_entries(
22
+ data: OrderedDict[K, V],
23
+ defaults: Mapping[K, V] | None = None,
24
+ treat_zero_as_empty: bool = False,
25
+ treat_false_as_empty: bool = False,
26
+ treat_empty_string_as_empty: bool = True,
27
+ ) -> OrderedDict[K, V]: ...
28
+
29
+
30
+ @overload
31
+ def replace_empty_dict_entries(
32
+ data: dict[K, V],
33
+ defaults: Mapping[K, V] | None = None,
34
+ treat_zero_as_empty: bool = False,
35
+ treat_false_as_empty: bool = False,
36
+ treat_empty_string_as_empty: bool = True,
37
+ ) -> dict[K, V]: ...
38
+
39
+
40
+ def replace_empty_dict_entries(
41
+ data: MutableMapping[K, V],
42
+ defaults: Mapping[K, V] | None = None,
43
+ treat_zero_as_empty: bool = False,
44
+ treat_false_as_empty: bool = False,
45
+ treat_empty_string_as_empty: bool = True,
46
+ ) -> MutableMapping[K, V]:
20
47
  """
21
48
  Replace empty entries in a dictionary with defaults or remove them entirely.
22
49
 
@@ -60,4 +87,4 @@ def replace_empty_dict_entries(
60
87
  new_value = value
61
88
 
62
89
  result[key] = new_value
63
- return cast(DictType, result)
90
+ return result
@@ -43,26 +43,23 @@ class ExtendedJSONEncoder(json.JSONEncoder):
43
43
  serializer: Function that converts objects of this type to JSON-serializable data
44
44
 
45
45
  Raises:
46
- TypeError: If serializer is not callable
47
46
  ValueError: If type_ is a built-in JSON type
48
47
  """
49
- if not callable(serializer):
50
- raise TypeError("Serializer must be callable")
51
48
  if type_ in (str, int, float, bool, list, dict, type(None)):
52
49
  raise ValueError(f"Cannot override built-in JSON type: {type_.__name__}")
53
50
  cls._type_handlers[type_] = serializer
54
51
 
55
- def default(self, obj: Any) -> Any: # noqa: ANN401
52
+ def default(self, o: Any) -> Any: # noqa: ANN401
56
53
  # Check registered type handlers first
57
54
  for type_, handler in self._type_handlers.items():
58
- if isinstance(obj, type_):
59
- return handler(obj)
55
+ if isinstance(o, type_):
56
+ return handler(o)
60
57
 
61
58
  # Special case: dataclasses (requires is_dataclass check, not isinstance)
62
- if is_dataclass(obj) and not isinstance(obj, type):
63
- return asdict(obj) # Don't need recursive serialization
59
+ if is_dataclass(o) and not isinstance(o, type):
60
+ return asdict(o) # Don't need recursive serialization
64
61
 
65
- return super().default(obj)
62
+ return super().default(o)
66
63
 
67
64
 
68
65
  def json_dumps(data: Any, type_handlers: dict[type[Any], Callable[[Any], Any]] | None = None, **kwargs: Any) -> str: # noqa: ANN401
@@ -101,7 +98,7 @@ def _auto_register_optional_types() -> None:
101
98
  """Register handlers for optional dependencies if available."""
102
99
  # Pydantic models
103
100
  try:
104
- from pydantic import BaseModel # type: ignore[import-not-found]
101
+ from pydantic import BaseModel # type: ignore[import-not-found] # noqa: PLC0415
105
102
 
106
103
  ExtendedJSONEncoder.register(BaseModel, lambda obj: obj.model_dump())
107
104
  except ImportError:
@@ -33,7 +33,7 @@ def random_decimal(from_value: Decimal, to_value: Decimal) -> Decimal:
33
33
  from_int = int(from_value * multiplier)
34
34
  to_int = int(to_value * multiplier)
35
35
 
36
- random_int = random.randint(from_int, to_int)
36
+ random_int = random.randint(from_int, to_int) # nosec B311
37
37
  return Decimal(random_int) / Decimal(multiplier)
38
38
 
39
39
 
@@ -68,5 +68,5 @@ def random_datetime(
68
68
  if total_seconds == 0:
69
69
  return from_time
70
70
 
71
- random_seconds = random.uniform(0, total_seconds)
71
+ random_seconds = random.uniform(0, total_seconds) # nosec B311
72
72
  return from_time + timedelta(seconds=random_seconds)
@@ -0,0 +1,96 @@
1
+ import shlex
2
+ import subprocess # nosec
3
+ from dataclasses import dataclass
4
+
5
+ TIMEOUT_EXIT_CODE = 255
6
+
7
+
8
+ @dataclass
9
+ class CmdResult:
10
+ """Result of command execution."""
11
+
12
+ stdout: str
13
+ stderr: str
14
+ code: int
15
+
16
+ @property
17
+ def combined_output(self) -> str:
18
+ """Combined stdout and stderr output."""
19
+ result = ""
20
+ if self.stdout:
21
+ result += self.stdout
22
+ if self.stderr:
23
+ if result:
24
+ result += "\n"
25
+ result += self.stderr
26
+ return result
27
+
28
+
29
+ def run_cmd(
30
+ cmd: str,
31
+ timeout: int | None = 60,
32
+ capture_output: bool = True,
33
+ echo_command: bool = False,
34
+ shell: bool = False,
35
+ ) -> CmdResult:
36
+ """Execute a command.
37
+
38
+ Args:
39
+ cmd: Command to execute
40
+ timeout: Timeout in seconds, None for no timeout
41
+ capture_output: Whether to capture stdout/stderr
42
+ echo_command: Whether to print the command before execution
43
+ shell: If False (default), the command is parsed with shlex.split() and
44
+ executed without shell interpretation. Special characters like
45
+ backticks, $(), pipes (|), redirects (>, <), and wildcards (*) are
46
+ treated as literal text. This is the safe mode for commands with
47
+ user input.
48
+ If True, the command is passed to the shell as-is, enabling pipes,
49
+ redirects, command substitution, and other shell features. Use this
50
+ only for trusted commands that need shell functionality.
51
+
52
+ Returns:
53
+ CmdResult with stdout, stderr and exit code
54
+ """
55
+ if echo_command:
56
+ print(cmd) # noqa: T201
57
+ try:
58
+ if shell:
59
+ process = subprocess.run( # noqa: S602 # nosec
60
+ cmd, timeout=timeout, capture_output=capture_output, shell=True, check=False
61
+ )
62
+ else:
63
+ process = subprocess.run( # noqa: S603 # nosec
64
+ shlex.split(cmd), timeout=timeout, capture_output=capture_output, shell=False, check=False
65
+ )
66
+ stdout = process.stdout.decode("utf-8", errors="replace") if capture_output else ""
67
+ stderr = process.stderr.decode("utf-8", errors="replace") if capture_output else ""
68
+ return CmdResult(stdout=stdout, stderr=stderr, code=process.returncode)
69
+ except subprocess.TimeoutExpired:
70
+ return CmdResult(stdout="", stderr="timeout", code=TIMEOUT_EXIT_CODE)
71
+
72
+
73
+ def run_ssh_cmd(
74
+ host: str,
75
+ cmd: str,
76
+ ssh_key_path: str | None = None,
77
+ timeout: int = 60,
78
+ echo_command: bool = False,
79
+ ) -> CmdResult:
80
+ """Execute a command on remote host via SSH.
81
+
82
+ Args:
83
+ host: Remote host to connect to
84
+ cmd: Command to execute on remote host
85
+ ssh_key_path: Path to SSH private key file
86
+ timeout: Timeout in seconds
87
+ echo_command: Whether to print the command before execution
88
+
89
+ Returns:
90
+ CmdResult with stdout, stderr and exit code
91
+ """
92
+ ssh_cmd = "ssh -o 'StrictHostKeyChecking=no' -o 'LogLevel=ERROR'"
93
+ if ssh_key_path:
94
+ ssh_cmd += f" -i {shlex.quote(ssh_key_path)}"
95
+ ssh_cmd += f" {shlex.quote(host)} {shlex.quote(cmd)}"
96
+ return run_cmd(ssh_cmd, timeout=timeout, echo_command=echo_command)
@@ -0,0 +1,179 @@
1
+ from collections import OrderedDict, defaultdict
2
+ from decimal import Decimal
3
+
4
+ import pytest
5
+
6
+ from mm_std import replace_empty_dict_entries
7
+
8
+
9
+ @pytest.fixture
10
+ def sample_data():
11
+ return {"a": 1, "b": None, "c": "hello", "d": "", "e": 0, "f": False}
12
+
13
+
14
+ @pytest.fixture
15
+ def complex_data():
16
+ return {
17
+ "none": None,
18
+ "empty_str": "",
19
+ "zero_int": 0,
20
+ "zero_float": 0.0,
21
+ "zero_decimal": Decimal(0),
22
+ "false": False,
23
+ "keep_this": "value",
24
+ "keep_true": True,
25
+ "keep_one": 1,
26
+ }
27
+
28
+
29
+ class TestReplaceEmptyDictEntries:
30
+ def test_basic_none_removal(self, sample_data):
31
+ result = replace_empty_dict_entries(sample_data)
32
+ assert result == {"a": 1, "c": "hello", "e": 0, "f": False} # empty string removed by default
33
+ assert type(result) is dict
34
+
35
+ def test_none_replacement_with_defaults(self, sample_data):
36
+ defaults = {"b": 42}
37
+ result = replace_empty_dict_entries(sample_data, defaults)
38
+ assert result == {"a": 1, "b": 42, "c": "hello", "e": 0, "f": False} # empty string removed by default
39
+
40
+ @pytest.mark.parametrize(
41
+ "treat_empty_string_as_empty,expected_keys",
42
+ [
43
+ (True, {"a", "c", "e", "f"}), # default behavior - empty strings removed
44
+ (False, {"a", "c", "d", "e", "f"}), # empty strings kept
45
+ ],
46
+ )
47
+ def test_empty_string_handling(self, sample_data, treat_empty_string_as_empty, expected_keys):
48
+ result = replace_empty_dict_entries(sample_data, treat_empty_string_as_empty=treat_empty_string_as_empty)
49
+ assert set(result.keys()) == expected_keys
50
+
51
+ def test_empty_string_with_defaults(self, sample_data):
52
+ defaults = {"d": "default_value"}
53
+ result = replace_empty_dict_entries(sample_data, defaults)
54
+ assert result["d"] == "default_value"
55
+
56
+ @pytest.mark.parametrize(
57
+ "treat_zero_as_empty,expected_has_zero",
58
+ [
59
+ (False, True), # default - zeros kept
60
+ (True, False), # zeros removed
61
+ ],
62
+ )
63
+ def test_zero_handling(self, treat_zero_as_empty, expected_has_zero):
64
+ data = {"a": 1, "b": 0, "c": 0.0, "d": Decimal(0)}
65
+ result = replace_empty_dict_entries(data, treat_zero_as_empty=treat_zero_as_empty)
66
+
67
+ if expected_has_zero:
68
+ assert "b" in result and "c" in result and "d" in result
69
+ else:
70
+ assert "b" not in result and "c" not in result and "d" not in result
71
+
72
+ def test_zero_with_defaults(self):
73
+ data = {"a": 1, "b": 0, "c": 0.0, "d": Decimal(0)}
74
+ defaults = {"b": 10, "c": 3.14, "d": Decimal(100)}
75
+ result = replace_empty_dict_entries(data, defaults, treat_zero_as_empty=True)
76
+ assert result == {"a": 1, "b": 10, "c": 3.14, "d": Decimal(100)}
77
+
78
+ @pytest.mark.parametrize(
79
+ "treat_false_as_empty,expected_has_false",
80
+ [
81
+ (False, True), # default - False kept
82
+ (True, False), # False removed
83
+ ],
84
+ )
85
+ def test_false_handling(self, treat_false_as_empty, expected_has_false):
86
+ data = {"a": True, "b": False, "c": "hello"}
87
+ result = replace_empty_dict_entries(data, treat_false_as_empty=treat_false_as_empty)
88
+
89
+ if expected_has_false:
90
+ assert result["b"] is False
91
+ else:
92
+ assert "b" not in result
93
+
94
+ def test_false_with_defaults(self):
95
+ data = {"a": True, "b": False, "c": "hello"}
96
+ result = replace_empty_dict_entries(data, {"b": True}, treat_false_as_empty=True)
97
+ assert result == {"a": True, "b": True, "c": "hello"}
98
+
99
+ def test_bool_vs_int_precedence(self):
100
+ data = {"a": False, "b": 0, "c": True, "d": 1}
101
+ defaults = {"a": "false_default", "b": "zero_default"}
102
+ result = replace_empty_dict_entries(data, defaults, treat_zero_as_empty=True, treat_false_as_empty=True)
103
+ assert result == {"a": "false_default", "b": "zero_default", "c": True, "d": 1}
104
+
105
+ @pytest.mark.parametrize(
106
+ "dict_type,expected_type",
107
+ [
108
+ (dict, dict),
109
+ (lambda d: defaultdict(list, d), defaultdict),
110
+ (lambda d: OrderedDict(d.items()), OrderedDict),
111
+ ],
112
+ )
113
+ def test_type_preservation(self, sample_data, dict_type, expected_type):
114
+ data = dict_type(sample_data)
115
+ result = replace_empty_dict_entries(data)
116
+ assert isinstance(result, expected_type)
117
+
118
+ def test_defaultdict_factory_preservation(self):
119
+ data = defaultdict(list, {"a": [1, 2], "b": None, "c": []})
120
+ result = replace_empty_dict_entries(data)
121
+
122
+ assert isinstance(result, defaultdict)
123
+ assert result.default_factory is list
124
+ assert result == {"a": [1, 2], "c": []}
125
+
126
+ # Test that default_factory still works
127
+ result["new_key"].append("test") # pyright: ignore[reportArgumentType, reportOptionalMemberAccess]
128
+ assert result["new_key"] == ["test"]
129
+
130
+ def test_ordered_dict_order_preservation(self):
131
+ data = OrderedDict([("a", 1), ("b", None), ("c", 2)])
132
+ result = replace_empty_dict_entries(data)
133
+
134
+ assert type(result) is OrderedDict
135
+ assert list(result.keys()) == ["a", "c"]
136
+ assert result == OrderedDict([("a", 1), ("c", 2)])
137
+
138
+ def test_all_flags_combined(self, complex_data):
139
+ result = replace_empty_dict_entries(
140
+ complex_data, treat_zero_as_empty=True, treat_false_as_empty=True, treat_empty_string_as_empty=True
141
+ )
142
+ assert result == {"keep_this": "value", "keep_true": True, "keep_one": 1}
143
+
144
+ @pytest.mark.parametrize(
145
+ "input_dict,expected",
146
+ [
147
+ ({}, {}),
148
+ ({"a": 1, "b": "hello", "c": True}, {"a": 1, "b": "hello", "c": True}),
149
+ ],
150
+ )
151
+ def test_edge_cases(self, input_dict, expected):
152
+ result = replace_empty_dict_entries(input_dict)
153
+ assert result == expected
154
+ assert result is not input_dict # Should be a new instance
155
+
156
+ def test_partial_defaults(self):
157
+ data = {"a": None, "b": None, "c": 1}
158
+ defaults = {"a": "replaced"}
159
+ result = replace_empty_dict_entries(data, defaults)
160
+ assert result == {"a": "replaced", "c": 1}
161
+
162
+ @pytest.mark.parametrize(
163
+ "value,treat_zero_as_empty,should_be_removed",
164
+ [
165
+ (-0.0, True, True),
166
+ (+0.0, True, True),
167
+ (Decimal("0.00"), True, True),
168
+ (0.0000001, True, False),
169
+ (-1, True, False),
170
+ ],
171
+ )
172
+ def test_numeric_edge_cases(self, value, treat_zero_as_empty, should_be_removed):
173
+ data = {"test_key": value}
174
+ result = replace_empty_dict_entries(data, treat_zero_as_empty=treat_zero_as_empty)
175
+
176
+ if should_be_removed:
177
+ assert "test_key" not in result
178
+ else:
179
+ assert "test_key" in result