Skip to content

Commit d2deb96

Browse files
committed
make Console.get_args() intake **find_args kwargs instead of a find_args dictionary
1 parent 0b493fb commit d2deb96

6 files changed

Lines changed: 58 additions & 92 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ cython_debug/
2525
__pypackages__/
2626

2727
# TESTING
28+
.venv/
2829
.pytest_cache/
2930

3031
# MISCELLANEOUS

src/xulbux/console.py

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -101,16 +101,14 @@ def __bool__(self):
101101

102102
class Args:
103103
"""Container for parsed command-line arguments, allowing attribute-style access.\n
104-
--------------------------------------------------------------------------------------
105-
- `kwargs` -⠀a mapping of argument aliases to their corresponding data dictionaries\n
106-
--------------------------------------------------------------------------------------
104+
----------------------------------------------------------------------------------------
105+
- `**kwargs` -⠀a mapping of argument aliases to their corresponding data dictionaries\n
106+
----------------------------------------------------------------------------------------
107107
For example, if an argument `foo` was parsed, it can be accessed via `args.foo`.
108108
Each such attribute (e.g. `args.foo`) is an instance of `ArgResult`."""
109109

110-
def __init__(self, **kwargs: Mapping[str, str | list[str]]):
110+
def __init__(self, **kwargs: ArgResultRegular | ArgResultPositional):
111111
for alias_name, data_dict in kwargs.items():
112-
if not alias_name.isidentifier():
113-
raise TypeError(f"Argument alias '{alias_name}' is invalid: It must be a valid Python variable name.")
114112
if "values" in data_dict:
115113
setattr(
116114
self, alias_name,
@@ -181,70 +179,70 @@ class Console:
181179

182180
@staticmethod
183181
def get_args(
184-
find_args: Mapping[str, set[str] | ArgConfigWithDefault | Literal["before", "after"]],
185182
allow_spaces: bool = False,
183+
**find_args: set[str] | ArgConfigWithDefault | Literal["before", "after"],
186184
) -> Args:
187185
"""Will search for the specified arguments in the command line
188186
arguments and return the results as a special `Args` object.\n
189-
-----------------------------------------------------------------------------------------------------------
190-
- `find_args` -⠀a dictionary defining the argument aliases and their flags/configuration (explained below)
191-
- `allow_spaces` -⠀if true , flagged argument values can span multiple space-separated tokens until the
187+
---------------------------------------------------------------------------------------------------------
188+
- `allow_spaces` -⠀if true, flagged argument values can span multiple space-separated tokens until the
192189
next flag is encountered, otherwise only the immediate next token is captured as the value:<br>
193190
This allows passing multi-word values without quotes
194191
(e.g. `-f hello world` instead of `-f "hello world"`).<br>
195192
* This setting does not affect `"before"`/`"after"` positional arguments,
196193
which always treat each token separately.<br>
197194
* When `allow_spaces=True`, positional `"after"` arguments will always be empty if any flags
198-
are present, as all tokens following the last flag are consumed as that flag's value.\n
199-
-----------------------------------------------------------------------------------------------------------
200-
The `find_args` dictionary can have the following structures for each alias:
195+
are present, as all tokens following the last flag are consumed as that flag's value.
196+
- `**find_args` -⠀kwargs defining the argument aliases and their flags/configuration (explained below)\n
197+
---------------------------------------------------------------------------------------------------------
198+
The `**find_args` keyword arguments can have the following structures for each alias:
201199
1. Simple set of flags (when no default value is needed):
202200
```python
203-
"alias_name": {"-f", "--flag"}
201+
alias_name={"-f", "--flag"}
204202
```
205203
2. Dictionary with `"flags"` and `"default"` value:
206204
```python
207-
"alias_name": {
205+
alias_name={
208206
"flags": {"-f", "--flag"},
209207
"default": "some_value",
210208
}
211209
```
212210
3. Positional argument collection using the literals `"before"` or `"after"`:
213211
```python
214-
"alias_name": "before" # Collects non-flagged args before first flag
215-
"alias_name": "after" # Collects non-flagged args after last flag
212+
alias_name="before" # Collects non-flagged args before first flag
213+
alias_name="after" # Collects non-flagged args after last flag
216214
```
217-
#### Example `find_args`:
215+
#### Example usage:
218216
```python
219-
find_args={
220-
"text": "before", # Positional args before flagged args
221-
"arg1": {"-a1", "--arg1"}, # Just flags
222-
"arg2": {"-a2", "--arg2"}, # Just flags
223-
"arg3": { # With default value
217+
ARGS = Console.get_args(
218+
text="before", # Positional args before flagged args
219+
arg1={"-a1", "--arg1"}, # Just flags
220+
arg2={"-a2", "--arg2"}, # Just flags
221+
arg3={ # With default value
224222
"flags": {"-a3", "--arg3"},
225223
"default": "default_val",
226224
},
227-
}
225+
)
228226
```
229227
If the script is called via the command line:\n
230228
`python script.py Hello World -a1 "value1" --arg2`\n
231-
...it would return an `Args` object where:
232-
- `args.text.exists` is `True`, `args.text.values` is `["Hello", "World"]`
233-
- `args.arg1.exists` is `True`, `args.arg1.value` is `"value1"` (flag present with value)
234-
- `args.arg2.exists` is `True`, `args.arg2.value` is `None` (flag present without value)
235-
- `args.arg3.exists` is `False`, `args.arg3.value` is `"default_val"` (not present, has default value)\n
236-
-----------------------------------------------------------------------------------------------------------
229+
... it would return an `Args` object where:
230+
- `ARGS.text.exists` is `True`, `ARGS.text.values` is `["Hello", "World"]`
231+
- `ARGS.arg1.exists` is `True`, `ARGS.arg1.value` is `"value1"` (flag present with value)
232+
- `ARGS.arg2.exists` is `True`, `ARGS.arg2.value` is `None` (flag present without value)
233+
- `ARGS.arg3.exists` is `False`, `ARGS.arg3.value` is `"default_val"` (not present, has default value)\n
234+
---------------------------------------------------------------------------------------------------------
237235
If an arg, defined with flags in `find_args`, is NOT present in the command line:
238236
* `exists` will be `False`
239237
* `value` will be the specified `default` value, or `None` if no default was specified
240238
* `values` will be `[]` for positional `"before"`/`"after"` arguments\n
241-
-----------------------------------------------------------------------------------------------------------
239+
---------------------------------------------------------------------------------------------------------
242240
For positional arguments:
243241
- `"before"` collects all non-flagged arguments that appear before the first flag
244242
- `"after"` collects all non-flagged arguments that appear after the last flag's value
245-
-----------------------------------------------------------------------------------------------------------
246-
Normally if `allow_spaces` is false, it will take a space as the end of an args value. If it is true,
247-
it will take spaces as part of the value up until the next arg-flag is found.
243+
---------------------------------------------------------------------------------------------------------
244+
Normally if `allow_spaces` is false, it will take a space as the end of an args value.
245+
If it is true, it will take spaces as part of the value up until the next arg-flag is found.
248246
(Multiple spaces will become one space in the value.)"""
249247
positional_configs, arg_lookup, results = {}, {}, {}
250248
before_count, after_count = 0, 0
@@ -254,8 +252,6 @@ def get_args(
254252
for alias, config in find_args.items():
255253
flags, default_value = None, None
256254

257-
if not alias.isidentifier():
258-
raise TypeError(f"Argument alias '{alias}' is invalid: It must be a valid Python variable name.")
259255
if isinstance(config, str):
260256
# HANDLE POSITIONAL ARGUMENT COLLECTION
261257
if config == "before":
@@ -277,7 +273,7 @@ def get_args(
277273
flags = config
278274
results[alias] = {"exists": False, "value": default_value}
279275
elif isinstance(config, dict):
280-
flags, default_value = config["flags"], config["default"]
276+
flags, default_value = config.get("flags"), config.get("default")
281277
results[alias] = {"exists": False, "value": default_value}
282278
else:
283279
raise TypeError(

src/xulbux/regex.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,19 +202,19 @@ def _clean(pattern: str) -> str:
202202

203203
class LazyRegex:
204204
"""A class that lazily compiles and caches regex patterns on first access.\n
205-
-----------------------------------------------------------------------------------
206-
- `patterns` -⠀keyword arguments where the key is the name of the pattern and
205+
--------------------------------------------------------------------------------
206+
- `**patterns` -⠀keyword arguments where the key is the name of the pattern and
207207
the value is the regex pattern string to compile\n
208-
-----------------------------------------------------------------------------------
208+
--------------------------------------------------------------------------------
209209
#### Example usage:
210210
```python
211211
PATTERNS = LazyRegex(
212212
email=r"(?i)[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}",
213213
phone=r"\\+?\\d{1,3}[-.\\s]?\\(?\\d{1,4}\\)?[-.\\s]?\\d{1,4}[-.\\s]?\\d{1,9}",
214214
)
215215
216-
email_pattern = PATTERNS.email # COMPILES AND CACHES THE EMAIL PATTERN
217-
phone_pattern = PATTERNS.phone # COMPILES AND CACHES THE PHONE PATTERN
216+
email_pattern = PATTERNS.email # Compiles and caches the EMAIL pattern
217+
phone_pattern = PATTERNS.phone # Compiles and caches the PHONE pattern
218218
```"""
219219

220220
def __init__(self, **patterns: str):

tests/test_console.py

Lines changed: 19 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def test_console_size(mock_terminal_size):
213213
)
214214
def test_get_args_no_spaces(monkeypatch, argv, find_args, expected_args_dict):
215215
monkeypatch.setattr(sys, "argv", argv)
216-
args_result = Console.get_args(find_args, allow_spaces=False)
216+
args_result = Console.get_args(allow_spaces=False, **find_args)
217217
assert isinstance(args_result, Args)
218218
assert args_result.dict() == expected_args_dict
219219
for key, expected in expected_args_dict.items():
@@ -336,7 +336,7 @@ def test_get_args_no_spaces(monkeypatch, argv, find_args, expected_args_dict):
336336
)
337337
def test_get_args_with_spaces(monkeypatch, argv, find_args, expected_args_dict):
338338
monkeypatch.setattr(sys, "argv", argv)
339-
args_result = Console.get_args(find_args, allow_spaces=True)
339+
args_result = Console.get_args(allow_spaces=True, **find_args)
340340
assert isinstance(args_result, Args)
341341
assert args_result.dict() == expected_args_dict
342342

@@ -345,63 +345,37 @@ def test_get_args_flag_without_value(monkeypatch):
345345
"""Test that flags without values have None as their value, not True."""
346346
# TEST SINGLE FLAG WITHOUT VALUE AT END OF ARGS
347347
monkeypatch.setattr(sys, "argv", ["script.py", "--verbose"])
348-
args_result = Console.get_args({"verbose": {"--verbose"}})
348+
args_result = Console.get_args(verbose={"--verbose"})
349349
assert args_result.verbose.exists is True
350350
assert args_result.verbose.value is None
351351

352352
# TEST FLAG WITHOUT VALUE FOLLOWED BY ANOTHER FLAG
353353
monkeypatch.setattr(sys, "argv", ["script.py", "--verbose", "--debug"])
354-
args_result = Console.get_args({"verbose": {"--verbose"}, "debug": {"--debug"}})
354+
args_result = Console.get_args(verbose={"--verbose"}, debug={"--debug"})
355355
assert args_result.verbose.exists is True
356356
assert args_result.verbose.value is None
357357
assert args_result.debug.exists is True
358358
assert args_result.debug.value is None
359359

360360
# TEST FLAG WITH DEFAULT VALUE BUT NO PROVIDED VALUE
361361
monkeypatch.setattr(sys, "argv", ["script.py", "--mode"])
362-
args_result = Console.get_args({"mode": {"flags": {"--mode"}, "default": "production"}})
362+
args_result = Console.get_args(mode={"flags": {"--mode"}, "default": "production"})
363363
assert args_result.mode.exists is True
364364
assert args_result.mode.value is None
365365

366366

367-
def test_get_args_invalid_alias():
368-
with pytest.raises(TypeError, match="Argument alias 'invalid-alias' is invalid."):
369-
Args(**{"invalid-alias": {"exists": False, "value": None}})
370-
371-
with pytest.raises(TypeError, match="Argument alias '123start' is invalid."):
372-
Args(**{"123start": {"exists": False, "value": None}})
373-
374-
375-
def test_get_args_invalid_config():
376-
with pytest.raises(TypeError, match="Invalid configuration type for alias 'bad_config'.\n"
377-
"Must be a set, dict, literal 'before' or literal 'after'."):
378-
Console.get_args({"bad_config": 123}) # type: ignore[assignment]
379-
380-
with pytest.raises(ValueError,
381-
match="Invalid configuration for alias 'missing_flags'. Dictionary must contain a 'flags' key."):
382-
Console.get_args({"missing_flags": {"default": "value"}}) # type: ignore[assignment]
383-
384-
with pytest.raises(ValueError,
385-
match="Invalid configuration for alias 'bad_flags'. Dictionary must contain a 'default' key.\n"
386-
"Use a simple set of strings if no default value is needed and only flags are to be specified."):
387-
Console.get_args({"bad_flags": {"flags": ["--flag"]}}) # type: ignore[assignment]
388-
389-
with pytest.raises(ValueError, match="Invalid 'flags' for alias 'bad_flags'. Must be a set of strings."):
390-
Console.get_args({"bad_flags": {"flags": "not-a-set", "default": "value"}}) # type: ignore[assignment]
391-
392-
393367
def test_get_args_duplicate_flag():
394368
with pytest.raises(ValueError, match="Duplicate flag '-f' found. It's assigned to both 'file1' and 'file2'."):
395-
Console.get_args({"file1": {"-f", "--file1"}, "file2": {"flags": {"-f", "--file2"}, "default": "..."}})
369+
Console.get_args(file1={"-f", "--file1"}, file2={"flags": {"-f", "--file2"}, "default": "..."})
396370

397371
with pytest.raises(ValueError, match="Duplicate flag '--long' found. It's assigned to both 'arg1' and 'arg2'."):
398-
Console.get_args({"arg1": {"flags": {"--long"}, "default": "..."}, "arg2": {"-a", "--long"}})
372+
Console.get_args(arg1={"flags": {"--long"}, "default": "..."}, arg2={"-a", "--long"})
399373

400374

401375
def test_get_args_dash_values_not_treated_as_flags(monkeypatch):
402376
"""Test that values starting with dashes are not treated as flags unless explicitly defined"""
403377
monkeypatch.setattr(sys, "argv", ["script.py", "-v", "-42", "--input", "-3.14"])
404-
result = Console.get_args({"verbose": {"-v"}, "input": {"--input"}})
378+
result = Console.get_args(verbose={"-v"}, input={"--input"})
405379

406380
assert result.verbose.exists is True
407381
assert result.verbose.value == "-42"
@@ -412,7 +386,7 @@ def test_get_args_dash_values_not_treated_as_flags(monkeypatch):
412386
def test_get_args_dash_strings_as_values(monkeypatch):
413387
"""Test that dash-prefixed strings are treated as values when not defined as flags"""
414388
monkeypatch.setattr(sys, "argv", ["script.py", "-f", "--not-a-flag", "-t", "-another-value"])
415-
result = Console.get_args({"file": {"-f"}, "text": {"-t"}})
389+
result = Console.get_args(file={"-f"}, text={"-t"})
416390

417391
assert result.file.exists is True
418392
assert result.file.value == "--not-a-flag"
@@ -423,7 +397,7 @@ def test_get_args_dash_strings_as_values(monkeypatch):
423397
def test_get_args_positional_with_dashes_before(monkeypatch):
424398
"""Test that positional 'before' arguments include dash-prefixed values"""
425399
monkeypatch.setattr(sys, "argv", ["script.py", "-123", "--some-file", "normal", "-v"])
426-
result = Console.get_args({"before_args": "before", "verbose": {"-v"}})
400+
result = Console.get_args(before_args="before", verbose={"-v"})
427401

428402
assert result.before_args.exists is True
429403
assert result.before_args.values == ["-123", "--some-file", "normal"]
@@ -434,7 +408,7 @@ def test_get_args_positional_with_dashes_before(monkeypatch):
434408
def test_get_args_positional_with_dashes_after(monkeypatch):
435409
"""Test that positional 'after' arguments include dash-prefixed values"""
436410
monkeypatch.setattr(sys, "argv", ["script.py", "-v", "value", "-123", "--output-file", "-negative"])
437-
result = Console.get_args({"verbose": {"-v"}, "after_args": "after"})
411+
result = Console.get_args(verbose={"-v"}, after_args="after")
438412

439413
assert result.verbose.exists is True
440414
assert result.verbose.value == "value"
@@ -445,7 +419,7 @@ def test_get_args_positional_with_dashes_after(monkeypatch):
445419
def test_get_args_multiword_with_dashes(monkeypatch):
446420
"""Test multiword values with dashes when allow_spaces=True"""
447421
monkeypatch.setattr(sys, "argv", ["script.py", "-m", "start", "-middle", "--end", "-f", "other"])
448-
result = Console.get_args({"message": {"-m"}, "file": {"-f"}}, allow_spaces=True)
422+
result = Console.get_args(allow_spaces=True, message={"-m"}, file={"-f"})
449423

450424
assert result.message.exists is True
451425
assert result.message.value == "start -middle --end"
@@ -461,13 +435,13 @@ def test_get_args_mixed_dash_scenarios(monkeypatch):
461435
"after1", "-also-not-flag"
462436
]
463437
)
464-
result = Console.get_args({
465-
"before": "before",
466-
"verbose": {"-v"},
467-
"debug": {"-d"},
468-
"file": {"--file"},
469-
"after": "after",
470-
})
438+
result = Console.get_args(
439+
before="before",
440+
verbose={"-v"},
441+
debug={"-d"},
442+
file={"--file"},
443+
after="after",
444+
)
471445

472446
assert result.before.exists is True
473447
assert result.before.values == ["before1", "-not-flag", "before2"]

tests/test_data.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,6 @@ def test_serialize_bytes():
4242
import base64
4343
assert base64.b64decode(serialized_non_utf8["bytes"]).decode("latin-1") == non_utf8_bytes.decode("latin-1")
4444

45-
with pytest.raises(TypeError):
46-
Data.serialize_bytes("not bytes") # type: ignore[assignment]
47-
4845

4946
def test_deserialize_bytes():
5047
utf8_serialized_bytes = {"bytes": "Hello", "encoding": "utf-8"}

tests/test_path.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,6 @@ def test_extend(setup_test_environment):
6262
assert Path.extend("") is None
6363
with pytest.raises(PathNotFoundError, match="Path is empty."):
6464
Path.extend("", raise_error=True)
65-
with pytest.raises(TypeError, match="parameter must be a string"):
66-
Path.extend(None, raise_error=True) # type: ignore[assignment]
6765

6866
# FOUND IN STANDARD LOCATIONS
6967
assert Path.extend("file_in_cwd.txt") == str(env["cwd"] / "file_in_cwd.txt")

0 commit comments

Comments
 (0)