# argument

Parse user arguments and verify them.

{{< katex />}}

## check\_input

Check the existence, type, and extension of a file path.

### Parameters

**path : `str`**

The path to the file to validate.

**extension : `list[str]] | None`, optional**

A list of allowed file extensions (including the leading dot, like
`.txt`). If `None`, extension validation is skipped. By default,
`None`.

### Returns

**`pathlib.Path`**

The validated and expanded path to the file.

### Raises

**`ValueError`**

- If the file does not exist.
- If the path exists but is not a file.
- If the file extension is not in the allowed list.

{{% details title="Code block details" open=false %}}
```python
def check_input(path: str, extension: list[str] | None = None) -> pathlib.Path:
    """Check the existence, type, and extension of a file path.

    Parameters
    ----------
    path : `str`
        The path to the file to validate.

    extension : `list[str]] | None`, optional
        A list of allowed file extensions (including the leading dot, like
        `.txt`). If `None`, extension validation is skipped. By default,
        `None`.

    Returns
    -------
    `pathlib.Path`
        The validated and expanded path to the file.

    Raises
    ------
    `ValueError`
        - If the file does not exist.
        - If the path exists but is not a file.
        - If the file extension is not in the allowed list.
    """
    file: pathlib.Path = pathlib.Path(path).expanduser()

    if not file.exists():
        Error.FILE_NOT_FOUND(argparse.ArgumentTypeError, file=file)

    if not file.is_file():
        Error.NOT_DIRECTORY(argparse.ArgumentTypeError, file=file)

    if extension is not None and file.suffix.lower() not in extension:
        format_list: str = ".\n- ".join(extension)
        Error.WRONG_EXTENSION(
            argparse.ArgumentTypeError, type="input", format_list=format_list
        )

    return file
```
{{% /details %}}
## check\_output

Check the existence, type, and extension of a file path.

### Parameters

**path : `str`**

The path to the file to validate.

**extension : `typing.Optional[list[str]]`, optional**

A list of allowed file extensions (including the leading dot, like
`.txt`). If `None`, extension validation is skipped. By default,
`None`.

### Returns

**`pathlib.Path`**

The validated and expanded path to the file.

### Raises

**`ValueError`**

- If the file does not exist.
- If the path exists but is not a file.
- If the file extension is not in the allowed list.

{{% details title="Code block details" open=false %}}
```python
def check_output(
    path: str, extension: typing.Optional[list[str]] = None
) -> pathlib.Path:
    """Check the existence, type, and extension of a file path.

    Parameters
    ----------
    path : `str`
        The path to the file to validate.

    extension : `typing.Optional[list[str]]`, optional
        A list of allowed file extensions (including the leading dot, like
        `.txt`). If `None`, extension validation is skipped. By default,
        `None`.

    Returns
    -------
    `pathlib.Path`
        The validated and expanded path to the file.

    Raises
    ------
    `ValueError`
        - If the file does not exist.
        - If the path exists but is not a file.
        - If the file extension is not in the allowed list.
    """
    file: pathlib.Path = pathlib.Path(path).expanduser()

    if not file.parent.exists():
        Error.PATH_NOT_FOUND(argparse.ArgumentTypeError, path=file.parent)

    sys.__excepthook__ = Error.except_hook

    if extension is not None and file.suffix.lower() not in extension:
        format_list: str = ".\n- ".join(extension)
        Error.WRONG_EXTENSION(
            argparse.ArgumentTypeError,
            file=file,
            type="output",
            format_list=format_list,
        )

    return file
```
{{% /details %}}
## HydrogenOption

Enumeration defining hydrogen addition options for donor atoms.

{{% details title="Code block details" open=false %}}
```python
class HydrogenOption(enum.Enum):
    """Enumeration defining hydrogen addition options for donor atoms."""

    NONE = 0
    FIRST = 1
    SECOND = 2
    BOTH = 3

    @classmethod
    def get_list(cls, value: int | str) -> list[bool] | None:
        """Convert a hydrogen option value (integer or string) into boolean
        flags.

        Parameters
        ----------
        value : `int` | `str`
            The hydrogen option, either as an integer or a string. Options are:

            - `0` or `'none'` → no hydrogens
            - `1` or `'first'` → only the first hydrogen
            - `2` or `'second'` → only the second hydrogen
            - `3` or `'both'` → both hydrogens

        Returns
        -------
        `list[bool]` | `None`
            A list of two boolean values representing which hydrogen positions
            are active, or `None` if the provided value is invalid.

        Raises
        ------
        `argparse.ArgumentTypeError`
            - If the provided `value` is not a valid hydrogen option.
        """
        if hasattr(cls, str(value).upper()):
            value = getattr(cls, str(value).upper()).value

        try:
            return HydrogenOption.__set_list(value)
        except ValueError:
            except_value: str = ""

            for enum_value in cls:
                except_value += (
                    f"- {enum_value.value} or '{enum_value.name.lower()}'.\n"
                )

            Error.ADD_HYDROGEN(
                argparse.ArgumentTypeError,
                value=value,
                except_value=except_value[:-1],
            )

        return None

    @staticmethod
    def __set_list(value: int) -> list[bool]:
        """Convert an integer option value into a boolean list representation.

        Parameters
        ----------
        value : `int`
            The numeric hydrogen option (0–3).

        Returns
        -------
        `list[bool]`
            A list of two booleans corresponding to hydrogen inclusion for the
            first and second hydrogen positions, respectively.

        Raises
        ------
        `ValueError`
            - If the `value` does not correspond to a valid hydrogen option.
        """
        match int(value):
            case 0:
                return [False, False]
            case 1:
                return [True, False]
            case 2:
                return [False, True]

        return [True, True]
```
{{% /details %}}
### get\_list

Convert a hydrogen option value (integer or string) into boolean
flags.

#### Parameters

**value : `int` | `str`**

The hydrogen option, either as an integer or a string. Options are:

- `0` or `'none'` → no hydrogens
- `1` or `'first'` → only the first hydrogen
- `2` or `'second'` → only the second hydrogen
- `3` or `'both'` → both hydrogens

#### Returns

**`list[bool]` | `None`**

A list of two boolean values representing which hydrogen positions
are active, or `None` if the provided value is invalid.

#### Raises

**`argparse.ArgumentTypeError`**

- If the provided `value` is not a valid hydrogen option.

{{% details title="Code block details" open=false %}}
```python
def get_list(cls, value: int | str) -> list[bool] | None:
        """Convert a hydrogen option value (integer or string) into boolean
        flags.

        Parameters
        ----------
        value : `int` | `str`
            The hydrogen option, either as an integer or a string. Options are:

            - `0` or `'none'` → no hydrogens
            - `1` or `'first'` → only the first hydrogen
            - `2` or `'second'` → only the second hydrogen
            - `3` or `'both'` → both hydrogens

        Returns
        -------
        `list[bool]` | `None`
            A list of two boolean values representing which hydrogen positions
            are active, or `None` if the provided value is invalid.

        Raises
        ------
        `argparse.ArgumentTypeError`
            - If the provided `value` is not a valid hydrogen option.
        """
        if hasattr(cls, str(value).upper()):
            value = getattr(cls, str(value).upper()).value

        try:
            return HydrogenOption.__set_list(value)
        except ValueError:
            except_value: str = ""

            for enum_value in cls:
                except_value += (
                    f"- {enum_value.value} or '{enum_value.name.lower()}'.\n"
                )

            Error.ADD_HYDROGEN(
                argparse.ArgumentTypeError,
                value=value,
                except_value=except_value[:-1],
            )

        return None
```
{{% /details %}}
### \_\_set\_list

Convert an integer option value into a boolean list representation.

#### Parameters

**value : `int`**

The numeric hydrogen option (0–3).

#### Returns

**`list[bool]`**

A list of two booleans corresponding to hydrogen inclusion for the
first and second hydrogen positions, respectively.

#### Raises

**`ValueError`**

- If the `value` does not correspond to a valid hydrogen option.

{{% details title="Code block details" open=false %}}
```python
def __set_list(value: int) -> list[bool]:
        """Convert an integer option value into a boolean list representation.

        Parameters
        ----------
        value : `int`
            The numeric hydrogen option (0–3).

        Returns
        -------
        `list[bool]`
            A list of two booleans corresponding to hydrogen inclusion for the
            first and second hydrogen positions, respectively.

        Raises
        ------
        `ValueError`
            - If the `value` does not correspond to a valid hydrogen option.
        """
        match int(value):
            case 0:
                return [False, False]
            case 1:
                return [True, False]
            case 2:
                return [False, True]

        return [True, True]
```
{{% /details %}}
## OwnArgumentParser

Custom argument parser extending `argparse.ArgumentParser`.

This subclass overrides the default error handling method to
raise a custom error using the `Error.GENERAL_ARGUMENT_ERROR`
handler instead of printing to `stderr` and exiting.

{{% details title="Code block details" open=false %}}
```python
class OwnArgumentParser(argparse.ArgumentParser):
    """Custom argument parser extending `argparse.ArgumentParser`.

    This subclass overrides the default error handling method to
    raise a custom error using the `Error.GENERAL_ARGUMENT_ERROR`
    handler instead of printing to `stderr` and exiting.
    """

    def error(self, message: str):
        """Override the default error behavior to raise a custom error.

        Parameters
        ----------
        message : `str`
            The error message describing the argument parsing failure.

        Raises
        ------
        `argparse.ArgumentTypeError`
            - Always raised through `Error.GENERAL_ARGUMENT_ERROR`
              when an argument parsing error occurs.
        """
        Error.GENERAL_ARGUMENT_ERROR(
            argparse.ArgumentTypeError, message=message
        )
```
{{% /details %}}
### error

Override the default error behavior to raise a custom error.

#### Parameters

**message : `str`**

The error message describing the argument parsing failure.

#### Raises

**`argparse.ArgumentTypeError`**

- Always raised through `Error.GENERAL_ARGUMENT_ERROR`
  when an argument parsing error occurs.

{{% details title="Code block details" open=false %}}
```python
def error(self, message: str):
        """Override the default error behavior to raise a custom error.

        Parameters
        ----------
        message : `str`
            The error message describing the argument parsing failure.

        Raises
        ------
        `argparse.ArgumentTypeError`
            - Always raised through `Error.GENERAL_ARGUMENT_ERROR`
              when an argument parsing error occurs.
        """
        Error.GENERAL_ARGUMENT_ERROR(
            argparse.ArgumentTypeError, message=message
        )
```
{{% /details %}}
## parse\_argument

Parse argument and return them.

### Parameters

**version : `str`**

The current program version.

### Returns

**`argparse.Namespace`**

The parsed argument.

{{% details title="Code block details" open=false %}}
```python
def parse_argument(version: str) -> argparse.Namespace:
    """Parse argument and return them.

    Parameters
    ----------
    version : `str`
        The current program version.

    Returns
    -------
    `argparse.Namespace`
        The parsed argument.
    """
    # ==========================
    #
    # CREATE THE ARGUMENT PARSER
    #
    # ==========================
    description: str = """
      ██████ ▄▄▄█████▓ ██▀███   ▄▄▄       ███▄    █   ▄████ ▓█████ 
    ▒██    ▒ ▓  ██▒ ▓▒▓██ ▒ ██▒▒████▄     ██ ▀█   █  ██▒ ▀█▒▓█   ▀ 
    ░ ▓██▄   ▒ ▓██░ ▒░▓██ ░▄█ ▒▒██  ▀█▄  ▓██  ▀█ ██▒▒██░▄▄▄░▒███   
      ▒   ██▒░ ▓██▓ ░ ▒██▀▀█▄  ░██▄▄▄▄██ ▓██▒  ▐▌██▒░▓█  ██▓▒▓█  ▄ 
    ▒██████▒▒  ▒██▒ ░ ░██▓ ▒██▒ ▓█   ▓██▒▒██░   ▓██░░▒▓███▀▒░▒████▒
    ▒ ▒▓▒ ▒ ░  ▒ ░░   ░ ▒▓ ░▒▓░ ▒▒   ▓▒█░░ ▒░   ▒ ▒  ░▒   ▒ ░░ ▒░ ░
    ░ ░▒  ░ ░    ░      ░▒ ░ ▒░  ▒   ▒▒ ░░ ░░   ░ ▒░  ░   ░  ░ ░  ░
    ░  ░  ░    ░        ░░   ░   ░   ▒      ░   ░ ░ ░ ░   ░    ░   
          ░              ░           ░  ░         ░       ░    ░  ░


    Program to compute interaction between two molecules from their
    pharmacophore. The simplest command line is:

    \033[1m$ strange -i protein.pdb ligand.pdb -o interaction.csv\033[0m

    \033[7m Legend: \033[0m
        - int: Integer.
        - str: String.
        - [type|type][value]: Types of the input required, follow by the
            default value. So if this optional arguments is not used, "value"
            will be chosen.

    \033[7m Documentation: NONE YET \033[0m

    Note that if `--output` is not given, then `--pharmacophore` is mandatory.
    The program then will compute only pharmacophore for a given molecule /
    selection.
    """

    parser: OwnArgumentParser = OwnArgumentParser(
        prog="strange",
        description=textwrap.dedent(description),
        formatter_class=argparse.RawTextHelpFormatter,
        add_help=False,
    )

    # ====================
    # Mandatory parameters
    # ====================
    _ = parser.add_argument(
        "-i",
        "--input",
        nargs="+",
        type=check_input,
        required=True,
        metavar="[FILE]",
        help=(
            "\033[7m Mandatory \033[0m\n"
            "Path to input one or two molecular structure files.\n\n"
        ),
    )

    _ = parser.add_argument(
        "-o",
        "--output",
        default=None,
        type=lambda file: check_output(file, [".csv"]),
        required=True,
        metavar="[FILE][.csv]",
        help=(
            "\033[7m Mandatory \033[0m\n"
            "Path to output computed interactions.\n\n"
        ),
    )

    # ==================
    # Optinal parameters
    # ==================
    _ = parser.add_argument(
        "-h",
        "--help",
        action="help",
        help="Display this help message, then exit the program.\n\n",
    )

    _ = parser.add_argument(
        "-v",
        "--version",
        action="version",
        version=version,
        help="Display program version, then exit.\n\n",
    )

    _ = parser.add_argument(
        "-s",
        "--selection",
        default=["", ""],
        nargs="+",
        type=str,
        metavar="[str][None]",
        help=(
            "One or two strings for filtering molecules through 'MDAnalysis':"
            "\n- First string applied to first given structure."
            "\n- Second string applied to the second given structure."
            "\n- If only one structure is given, first and second selection "
            "\n  are applied to it.\n\n"
        ),
    )

    _ = parser.add_argument(
        "-p",
        "--parameter",
        default=None,
        type=lambda file: check_input(
            file, [".toml", ".json", ".yaml", ".yml"]
        ),
        metavar="[FILE]['.toml'|'.json'|'.yaml'|'.yml'][None]",
        help="Path to input parameter file.\n\n",
    )

    _ = parser.add_argument(
        "--pharmacophore",
        default=[],
        nargs="+",
        type=lambda file: check_output(file, [".csv"]),
        metavar="[FILE]['.csv'][None]",
        help=(
            "Path to output computed pharmacophores. If given, except two "
            "files as output.\n\n"
        ),
    )

    _ = parser.add_argument(
        "--feature_file",
        default=None,
        type=lambda file: check_input(file, [".fdef"]),
        metavar="[FILE]['.fdef'][None]",
        help="Path to input pharmacophore feature file.\n\n",
    )

    _ = parser.add_argument(
        "--visualization",
        default=None,
        type=lambda file: check_output(file, [".mvsj", ".pml"]),
        metavar="[FILE]['.mvsj'][None]",
        help=(
            "Path to output visualization file:"
            "\n- '.mvsj' to visualize using Mol*."
            "\n- '.pml' to visualize using PyMOL."
            "\n\n"
        ),
    )

    _ = parser.add_argument(
        "-a",
        "--add_hydrogen",
        default=[False, False],
        type=HydrogenOption.get_list,
        metavar="[int|str][0]",
        help=(
            "Which structure to add hydrogen:"
            "\n- 0 or 'none': no addition."
            "\n- 1 or 'first': first structure only."
            "\n- 2 or 'second': the second structure only."
            "\n- 3 or 'both': both structure."
            "\n- If only one structure is given, it is duplicated. Meaning "
            "\n  that 1 applied for this structure with first given "
            "\n  selection, if any. And that 2 applied for this structure "
            "\n  with second given selection, if any."
            "\n\n  In this case, it is advice to simply use 'both'.\n\n"
        ),
    )

    # =====================
    #
    # EVAL PARSED ARGUMENTS
    #
    # =====================
    argument: argparse.Namespace = parser.parse_args()

    LENGTH_TO_TEST: list[dict[str, list[int] | int | str]] = [
        {
            "excepted_length": [1, 2],
            "command": "-i/--input",
            "length": len(argument.input),
        },
        {
            "excepted_length": [0, 1, 2],
            "command": "-s/--selection",
            "length": len(argument.selection),
        },
        {
            "excepted_length": [0, 2],
            "command": "--pharmacophore",
            "length": len(argument.pharmacophore),
        },
    ]

    # Note that because '-i / --input' is mandatory, it cannot be equal to '0'.
    for exceptation in LENGTH_TO_TEST:
        if exceptation["length"] in exceptation["excepted_length"]:
            continue

        good_length: str = ""

        for length in exceptation["excepted_length"][:-1]:
            good_length += f"{length}, or "

        good_length += str(exceptation["excepted_length"][-1])

        Error.EXCEPTED_LEN(
            argparse.ArgumentTypeError,
            good_length=good_length,
            command=exceptation["command"],
            length=exceptation["length"],
        )

    if len(argument.pharmacophore) != 2:
        return argument

    if argument.pharmacophore[0] == argument.pharmacophore[1]:
        Error.SAME_PHARMA_OUTPUT(
            argparse.ArgumentTypeError,
            first=argument.pharmacophore[0],
            second=argument.pharmacophore[1],
        )

    return argument
```
{{% /details %}}
