Repository: SupImDos/pydantic-argparse

Issue:
Add possiblity to address the same option via several alias names
It would be nice to have the possibility to define in the `pydantic.BaseModel` Field definition the possible names for the cli arguments. It would be possible to have both: the short and long versions of the same argument.

example:
```
class Arguments(pydantic.BaseModel):
    debug: bool = Field (..., description="Shows the debug information", alias="my-custom-debug")
```
above would give the possibility to specify the argument in the command line as `command --my-custom-debug`  

How to add the short version of the same argument i.e: `-d`

If support for `aliases=('d', 'my-custom-debug')` would be added it would be nice as `pydantic.Field` does support extra arguments
Above would be interpreted to add the additional names to the parsed arguments

Discussion:
@SupImDos: Thanks @vadimbo 

I like this idea. I've been meaning to add more customisation via the extra `**kwargs` that `pydantic.Field` provides.

I will begin working on this as soon as I finish the big changes in #34.@donaldcampbelljr: Hi! Thank you for this project @SupImDos. I'd like to +1 this request for aliases so that we could have short arguments as well. Was curious if this enhancement is still planned? Thanks!@SupImDos: @donaldcampbelljr Certainly still on my radar, but couldn't say when I'll have time to do this.

If you (or someone else reading this) were interested in a PR, the following pointers could help:
* Update `pydantic_argparse.utils.arguments.name` to handle `aliases` provided in `field.field_info.extra`.
* Update `tests.utils.test_arguments.test_argument_name` tests for the new behaviour
* Add some new tests e.g., `tests.functional.test_aliases`

Some things to think about re: API / behaviour expectations
* What happens if you provide `alias` _and_ `aliases`?
* What is the behaviour for boolean arguments
* What is the behaviour for inverted arguments (i.e., `--no-<x>`)

Probably some other things I am forgetting here, but it should be relatively straight forward as long as its clear what the expectations are for the edge cases.


If you wanted a hack / escape hatch for now, you could try something like the following:
```python
# Create parser with model
parser = pydantic_argparse.ArgumentParser(model=...)

# Add `-s` short argument for existing `--string` argument
argument = parser._option_string_actions["--string"]
argument.option_strings = ("-s", "--string")
parser._option_string_actions["-s"] = argument
```

I haven't tested this, but to the point the `pydantic_argparse.ArgumentParser` is just a subclass of Python's real [`argparse.ArgumentParser`](https://docs.python.org/3/library/argparse.html#argumentparser-objects), so if you can update arguments for that it should be applicable here.@SupImDos: Partially completed in #73. Going to follow it up with another PR with some tests / docs if required.@SupImDos: This should now be possible in [`v0.10.0`](https://github.com/SupImDos/pydantic-argparse/releases/tag/v0.10.0)

Solution patch:
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
index 0b78775..1702e4a 100644
--- a/.github/workflows/format.yml
+++ b/.github/workflows/format.yml
@@ -18,7 +18,7 @@ jobs:
       - name: Checkout Repository
         uses: actions/checkout@v4
       - name: Setup uv
-        uses: astral-sh/setup-uv@v4
+        uses: astral-sh/setup-uv@v5
         with:
           enable-cache: true
           cache-dependency-glob: pyproject.toml
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 2ed2894..3fd013d 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -18,7 +18,7 @@ jobs:
       - name: Checkout Repository
         uses: actions/checkout@v4
       - name: Setup uv
-        uses: astral-sh/setup-uv@v4
+        uses: astral-sh/setup-uv@v5
         with:
           enable-cache: true
           cache-dependency-glob: pyproject.toml
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9b3dfec..77cba5b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -27,7 +27,7 @@ jobs:
       - name: Checkout Repository
         uses: actions/checkout@v4
       - name: Setup uv
-        uses: astral-sh/setup-uv@v4
+        uses: astral-sh/setup-uv@v5
         with:
           enable-cache: true
           cache-dependency-glob: pyproject.toml
@@ -50,6 +50,6 @@ jobs:
       - name: Run Tests
         run: hatch run test && hatch run coverage xml
       - name: Coverage Results
-        uses: codecov/codecov-action@v4
+        uses: codecov/codecov-action@v5
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/type.yml b/.github/workflows/type.yml
index ae8f226..7dea2d9 100644
--- a/.github/workflows/type.yml
+++ b/.github/workflows/type.yml
@@ -18,7 +18,7 @@ jobs:
       - name: Checkout Repository
         uses: actions/checkout@v4
       - name: Setup uv
-        uses: astral-sh/setup-uv@v4
+        uses: astral-sh/setup-uv@v5
         with:
           enable-cache: true
           cache-dependency-glob: pyproject.toml
diff --git a/pyproject.toml b/pyproject.toml
index 904e8a5..1ecf73d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -92,6 +92,7 @@ lint.select = [
     "RUF",  # ruff
 ]
 lint.ignore = [
+    "A005",  # allow shadowing of standard-library modules
     "D401",  # imperative mood - overly restrictive
     "S101",  # allow assertions
 ]
diff --git a/src/pydantic_argparse/parsers/boolean.py b/src/pydantic_argparse/parsers/boolean.py
index f12e8a1..ca7e0f7 100644
--- a/src/pydantic_argparse/parsers/boolean.py
+++ b/src/pydantic_argparse/parsers/boolean.py
@@ -53,7 +53,7 @@ def parse_field(
 
     # Add Boolean Field
     parser.add_argument(
-        utils.arguments.name(field, is_inverted),
+        *utils.arguments.names(field, is_inverted),
         action=action,
         help=utils.arguments.description(field),
         dest=field.alias,
diff --git a/src/pydantic_argparse/parsers/container.py b/src/pydantic_argparse/parsers/container.py
index dd304fe..b355e06 100644
--- a/src/pydantic_argparse/parsers/container.py
+++ b/src/pydantic_argparse/parsers/container.py
@@ -45,7 +45,7 @@ def parse_field(
     """
     # Add Container Field
     parser.add_argument(
-        utils.arguments.name(field),
+        *utils.arguments.names(field),
         action=argparse._StoreAction,
         nargs=argparse.ONE_OR_MORE,
         help=utils.arguments.description(field),
diff --git a/src/pydantic_argparse/parsers/enum.py b/src/pydantic_argparse/parsers/enum.py
index d643a48..43fd862 100644
--- a/src/pydantic_argparse/parsers/enum.py
+++ b/src/pydantic_argparse/parsers/enum.py
@@ -54,7 +54,7 @@ def parse_field(
 
     # Add Enum Field
     parser.add_argument(
-        utils.arguments.name(field, is_inverted),
+        *utils.arguments.names(field, is_inverted),
         action=action,
         help=utils.arguments.description(field),
         dest=field.alias,
diff --git a/src/pydantic_argparse/parsers/literal.py b/src/pydantic_argparse/parsers/literal.py
index eab449d..59dcae3 100644
--- a/src/pydantic_argparse/parsers/literal.py
+++ b/src/pydantic_argparse/parsers/literal.py
@@ -52,7 +52,7 @@ def parse_field(
 
     # Add Literal Field
     parser.add_argument(
-        utils.arguments.name(field, is_inverted),
+        *utils.arguments.names(field, is_inverted),
         action=action,
         help=utils.arguments.description(field),
         dest=field.alias,
diff --git a/src/pydantic_argparse/parsers/mapping.py b/src/pydantic_argparse/parsers/mapping.py
index 77da3e8..87bd74e 100644
--- a/src/pydantic_argparse/parsers/mapping.py
+++ b/src/pydantic_argparse/parsers/mapping.py
@@ -43,7 +43,7 @@ def parse_field(
     """
     # Add Mapping Field
     parser.add_argument(
-        utils.arguments.name(field),
+        *utils.arguments.names(field),
         action=argparse._StoreAction,
         help=utils.arguments.description(field),
         dest=field.alias,
diff --git a/src/pydantic_argparse/parsers/standard.py b/src/pydantic_argparse/parsers/standard.py
index e67b555..0471107 100644
--- a/src/pydantic_argparse/parsers/standard.py
+++ b/src/pydantic_argparse/parsers/standard.py
@@ -29,7 +29,7 @@ def parse_field(
     """
     # Add Standard Field
     parser.add_argument(
-        utils.arguments.name(field),
+        *utils.arguments.names(field),
         action=argparse._StoreAction,
         help=utils.arguments.description(field),
         dest=field.alias,
diff --git a/src/pydantic_argparse/utils/arguments.py b/src/pydantic_argparse/utils/arguments.py
index 91a083c..32bdbbc 100644
--- a/src/pydantic_argparse/utils/arguments.py
+++ b/src/pydantic_argparse/utils/arguments.py
@@ -7,7 +7,7 @@ names and formatting argument descriptions.
 from pydantic_argparse.compatibility import pydantic
 
 
-def name(field: pydantic.fields.ModelField, invert: bool = False) -> str:
+def names(field: pydantic.fields.ModelField, invert: bool = False) -> list[str]:
     """Standardises argument name.
 
     Args:
@@ -20,8 +20,17 @@ def name(field: pydantic.fields.ModelField, invert: bool = False) -> str:
     # Construct Prefix
     prefix = "--no-" if invert else "--"
 
+    flags = []
+
+    # Add custom aliases
+    aliases = field.field_info.extra.get("aliases", [])
+    for alias in aliases:
+        flags.append(f"{prefix}{alias.replace('_', '-')}")
+
     # Prepend prefix, replace '_' with '-'
-    return f"{prefix}{field.alias.replace('_', '-')}"
+    flags.append(f"{prefix}{field.alias.replace('_', '-')}")
+
+    return flags
 
 
 def description(field: pydantic.fields.ModelField) -> str:


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index bbc63b4..fb7d55e 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -48,6 +48,7 @@ def create_test_field(
     type: Type[Any] = str,  # noqa: A002
     default: Any = ...,
     description: Optional[str] = None,
+    aliases: Optional[list[str]] = None,
 ) -> pydantic.fields.ModelField:
     """Constructs a `pydantic` field with sensible defaults for testing.
 
@@ -56,6 +57,7 @@ def create_test_field(
         type (Type[Any]): Type of the field.
         default (Any): Default value for the field.
         description (Optional[str]): Description for the field.
+        aliases (Optional[list[str]]): List of flag aliases.
 
     Returns:
         pydantic.fields.ModelField: Dynamically constructed `pydantic` model.
@@ -63,7 +65,7 @@ def create_test_field(
     # Construct Pydantic Field
     return pydantic.fields.ModelField.infer(
         name=name,
-        value=pydantic.Field(default, description=description),
+        value=pydantic.Field(default, description=description, aliases=aliases),
         annotation=type,
         class_validators=None,
         config=pydantic.BaseConfig,
diff --git a/tests/utils/test_arguments.py b/tests/utils/test_arguments.py
index 75de660..7900cb6 100644
--- a/tests/utils/test_arguments.py
+++ b/tests/utils/test_arguments.py
@@ -15,18 +15,20 @@ from typing import Any, Optional
 @pytest.mark.parametrize(
     (
         "name",
+        "aliases",
         "invert",
         "expected",
     ),
     [
-        ("test", False, "--test"),
-        ("test", True, "--no-test"),
-        ("test_two", False, "--test-two"),
-        ("test_two", True, "--no-test-two"),
+        ("test", ["-t", "-te"], False, ["-t", "-te", "--test"]),
+        ("test", ["-nt"], True, ["-nt", "--no-test"]),
+        ("test_two", [], False, ["--test-two"]),
+        ("test_two", [], True, ["--no-test-two"]),
     ],
 )
-def test_argument_name(
+def test_argument_names(
     name: str,
+    aliases: list[str],
     invert: bool,
     expected: str,
 ) -> None:
@@ -34,14 +36,15 @@ def test_argument_name(
 
     Args:
         name (str): Argument name to test.
+        aliases (list[str]): List of aliases.
         invert (bool): Whether to invert the name.
         expected (str): Expected result of the test.
     """
     # Construct Pydantic Field
-    field = conf.create_test_field(name)
+    field = conf.create_test_field(name, aliases=aliases)
 
     # Generate Argument Name
-    result = utils.arguments.name(field, invert)
+    result = utils.arguments.names(field, invert)
 
     # Assert
     assert result == expected