diff --git a/CHANGELOG.md b/CHANGELOG.md index 9781cf8..e021ac8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ The version is represented by three digits: a.b.c. ## Unreleased +FEATURE: +- `cli`: add (minimal) command line interface ## \[0.1.1\] - 2024-06-10 diff --git a/README.md b/README.md index 9cd6492..aa07fc4 100644 --- a/README.md +++ b/README.md @@ -18,26 +18,34 @@ Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change, and give a look to the [contribution guidelines](https://github.com/VascoSch92/symmetria/blob/main/CONTRIBUTING.md). +--- + +- [Installation](#installation) +- [Quickstart](#quickstart) +- [Command Line Interface](#command-line-interface) +- [Overview](#overview) + +--- ## Installation Symmetria can be comfortably installed from PyPI using the command -```bash -pip install symmetria +```text +$ pip install symmetria ``` or directly from the source GitHub code with -```bash -pip install git+https://github.com/VascoSch92/symmetria@xxx +```text +$ pip install git+https://github.com/VascoSch92/symmetria@xxx ``` where `xxx` is the name of the branch or the tag you would like to install. You can check that `symmetria` was successfully installed by typing the command -```bash -symmetria --version +```text +$ symmetria --version ``` ## Quickstart @@ -171,6 +179,61 @@ in a nice formatted table: Click [here](https://symmetria.readthedocs.io/en/latest/pages/API_reference/elements/index.html) for an overview of all the functionalities implemented in `symmetria`. + +## Command Line Interface +Symmetria also provides a simple command line interface to find all what you need just with a line. + +```text +$ symmetria 132 ++------------------------------------------------------+ +| Permutation(1, 3, 2) | ++------------------------------------------------------+ +| order | 2 | ++---------------------------+--------------------------+ +| degree | 3 | ++---------------------------+--------------------------+ +| is derangement | False | ++---------------------------+--------------------------+ +| inverse | (1, 3, 2) | ++---------------------------+--------------------------+ +| parity | -1 (odd) | ++---------------------------+--------------------------+ +| cycle notation | (1)(2 3) | ++---------------------------+--------------------------+ +| cycle type | (1, 2) | ++---------------------------+--------------------------+ +| inversions | [(2, 3)] | ++---------------------------+--------------------------+ +| ascents | [1] | ++---------------------------+--------------------------+ +| descents | [2] | ++---------------------------+--------------------------+ +| excedencees | [2] | ++---------------------------+--------------------------+ +| records | [1, 2] | ++---------------------------+--------------------------+ +``` + +Check it out. + +```text +$ symmetria --help +Symmetria, an intuitive framework for working with the symmetric group and its elements. + + +Usage: symmetria [OPTIONS] + +Options: + -h, --help Print help + -v, --version Print version + +Argument (optional): + permutation A permutation you want to learn more about. + The permutation must be given in its one-line format, i.e., + for the permutation Permutation(2, 3, 1), write 231. +``` + + ## Overview | **Statistics** | ![Static Badge](https://img.shields.io/badge/symmetria-blue?style=for-the-badge) | diff --git a/docs/source/index.rst b/docs/source/index.rst index 614f2bc..1b71a82 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -92,26 +92,33 @@ with the symmetric group and its elements. .. toctree:: + :hidden: :maxdepth: 1 :caption: Getting started - :hidden: pages/getting_started/installation pages/getting_started/quickstart .. toctree:: + :hidden: :maxdepth: 2 :caption: API references - :hidden: pages/API_reference/elements/index pages/API_reference/groups/index pages/API_reference/generators/index .. toctree:: + :hidden: :maxdepth: 1 + :caption: Command Line Interface + + pages/cli/command_line_interface + +.. toctree:: :hidden: + :maxdepth: 1 :caption: Community pages/community/contributing.md diff --git a/docs/source/pages/cli/command_line_interface.rst b/docs/source/pages/cli/command_line_interface.rst new file mode 100644 index 0000000..693d6dc --- /dev/null +++ b/docs/source/pages/cli/command_line_interface.rst @@ -0,0 +1,56 @@ +Command Line Interface +====================== + +Symmetria also provides a simple command line interface to find all what you need just with a line. + +.. code-block:: text + + $ symmetria 132 + + +------------------------------------------------------+ + | Permutation(1, 3, 2) | + +------------------------------------------------------+ + | order | 2 | + +---------------------------+--------------------------+ + | degree | 3 | + +---------------------------+--------------------------+ + | is derangement | False | + +---------------------------+--------------------------+ + | inverse | (1, 3, 2) | + +---------------------------+--------------------------+ + | parity | -1 (odd) | + +---------------------------+--------------------------+ + | cycle notation | (1)(2 3) | + +---------------------------+--------------------------+ + | cycle type | (1, 2) | + +---------------------------+--------------------------+ + | inversions | [(2, 3)] | + +---------------------------+--------------------------+ + | ascents | [1] | + +---------------------------+--------------------------+ + | descents | [2] | + +---------------------------+--------------------------+ + | excedencees | [2] | + +---------------------------+--------------------------+ + | records | [1, 2] | + +---------------------------+--------------------------+ + +Check it out. + +.. code-block:: text + + $ symmetria --help + + Symmetria, an intuitive framework for working with the symmetric group and its elements. + + + Usage: symmetria [OPTIONS] + + Options: + -h, --help Print help + -v, --version Print version + + Argument (optional): + permutation A permutation you want to learn more about. + The permutation must be given in its one-line format, i.e., + for the permutation Permutation(2, 3, 1), write 231. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3b99a1b..0be8fe8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ requires = ["setuptools>61", "wheel", "toml", "build"] build-backend = "setuptools.build_meta" [project.scripts] -symmetria = "symmetria:_log_version" +symmetria = "symmetria.__main__:main" [tool.pytest.ini_options] cache_dir = "tests/.cache/pytest" diff --git a/symmetria/__init__.py b/symmetria/__init__.py index 468abab..490c545 100644 --- a/symmetria/__init__.py +++ b/symmetria/__init__.py @@ -1,5 +1,3 @@ -import sys - from symmetria.elements.cycle import Cycle from symmetria.generators.api import generate from symmetria.elements.permutation import Permutation @@ -7,21 +5,3 @@ __version__ = "0.1.1" __all__ = ["__version__", "generate", "Permutation", "Cycle", "CycleDecomposition"] - - -def _log_version() -> None: - """Private method which take a command line argument and log the version of `symmetria`.""" - if len(sys.argv) == 0 or len(sys.argv) == 1: - raise Exception("No command provided.") - elif len(sys.argv) == 2: - if sys.argv[1] == "--version": - print(f"v{__version__}") - else: - raise ValueError(f"command not found: {sys.argv[1]}") - - else: - raise ValueError(f"Expected 1 command, but got {len(sys.argv) -1}") - - -if __name__ == "__main__": - _log_version() diff --git a/symmetria/__main__.py b/symmetria/__main__.py new file mode 100644 index 0000000..a07ad74 --- /dev/null +++ b/symmetria/__main__.py @@ -0,0 +1,10 @@ +from symmetria.cli.cli import run_command_line_interface + + +def main() -> None: + """Entry point for the command line interface.""" + run_command_line_interface() + + +if __name__ == "__main__": + main() diff --git a/symmetria/cli/__init__.py b/symmetria/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/symmetria/cli/_commands.py b/symmetria/cli/_commands.py new file mode 100644 index 0000000..0013458 --- /dev/null +++ b/symmetria/cli/_commands.py @@ -0,0 +1,59 @@ +import sys + +from symmetria import Permutation, __version__ + + +class _Style: + """Class to define the output messages style.""" + + YELLOW: str = "\033[33m" + RED: str = "\033[31m" + END: str = "\033[0m" + BOLD: str = "\033[1m" + UNDERLINE: str = "\033[4m" + + +def _execute_error_message(message: str, exit_code: int) -> None: + """Print an error message in case of wrong given commands.""" + sys.stderr.write( + f"{_Style.RED}{_Style.BOLD}Error:{_Style.END} {message}. \n " + f"For more information, try `{_Style.YELLOW}--help{_Style.END}`, or `{_Style.YELLOW}-h{_Style.END}`. \n" + ) + sys.exit(exit_code) + + +def _execute_help_command() -> None: + """Execute the `--help`, or `-h`, command.""" + sys.stdout.write( + "Symmetria, an intuitive framework for working with the symmetric group and its elements.\n" + "\n" + f"{_Style.UNDERLINE}Usage:{_Style.END} symmetria [OPTIONS] \n" + "\n" + f"{_Style.UNDERLINE}Options:{_Style.END} \n" + " -h, --help Print help \n" + " -v, --version Print version \n" + "\n" + f"{_Style.UNDERLINE}Argument (optional):{_Style.END} \n" + " permutation A permutation you want to learn more about. \n" + " The permutation must be given in its one-line format, i.e., \n" + " for the permutation Permutation(2, 3, 1), write 231. \n" + ) + sys.exit(0) + + +def _execute_permutation_command(permutation: str) -> None: + """Execute the command when a permutation is given.""" + permutation = _parse_permutation(permutation=permutation) + sys.stdout.write(permutation.describe() + "\n") + sys.exit(0) + + +def _execute_version_command() -> None: + """Execute the `--version`, or `-v`, command.""" + sys.stdout.write(f"{_Style.BOLD}v{_Style.END}{__version__}" + "\n") + sys.exit(0) + + +def _parse_permutation(permutation: str) -> Permutation: + """Convert the provided string into a `Permutation` object.""" + return Permutation(*[int(d) for d in permutation]) diff --git a/symmetria/cli/cli.py b/symmetria/cli/cli.py new file mode 100644 index 0000000..05e4eb5 --- /dev/null +++ b/symmetria/cli/cli.py @@ -0,0 +1,56 @@ +import sys + +from symmetria.cli._commands import ( + _Style, + _execute_help_command, + _execute_error_message, + _execute_version_command, + _execute_permutation_command, +) + + +def run_command_line_interface() -> None: + """Run and manage the command line interface.""" + commands = sys.argv[1:] + + # case where no commands are provided + if len(commands) == 0: + _execute_error_message( + message="no command provided", + exit_code=1, + ) + + # case where one command is provided + elif len(commands) == 1: + command = commands[0] + if _is_a_flag(command=command): + if command in {"-h", "--help"}: + _execute_help_command() + elif command in {"-v", "--version"}: + _execute_version_command() + _execute_error_message( + message=f"unexpected argument `{_Style.YELLOW}{command}{_Style.END}` found", + exit_code=1, + ) + elif _is_a_permutation(command=command): + _execute_permutation_command(permutation=command) + _execute_error_message( + message=f"unexpected argument `{_Style.YELLOW}{command}{_Style.END}` found", + exit_code=1, + ) + + # otherwise + _execute_error_message( + message=f"Expected 1 command, but got {_Style.YELLOW}{len(sys.argv) - 1}{_Style.END} commands", + exit_code=1, + ) + + +def _is_a_flag(command: str) -> bool: + """Check if an argument has to be interpreted as a flag.""" + return command.startswith(("-", "--")) + + +def _is_a_permutation(command: str) -> bool: + """Check if an argument has to be interpreted as a permutation.""" + return command.isdigit() diff --git a/tests/tests_cli/__init__.py b/tests/tests_cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests_cli/test_cases.py b/tests/tests_cli/test_cases.py new file mode 100644 index 0000000..d396f32 --- /dev/null +++ b/tests/tests_cli/test_cases.py @@ -0,0 +1,26 @@ +from symmetria import Permutation + +TEST_IS_A_FLAG = [ + ("", False), + ("not-a-flag", False), + ("-flag", True), + ("--flag", True), + ("-_still a flag", True), + ("-_still-dr-dre", True), +] +TEST_IS_PERMUTATION = [ + ("", False), + ("asd", False), + ("12 34", False), + ("123-hello", False), + ("hello-world", False), + ("123,456", False), + ("13245", True), + ("1", True), + ("23451", True), +] +TEST_PARSE_PERMUTATION = [ + ("123", Permutation(1, 2, 3)), + ("2341", Permutation(2, 3, 4, 1)), + ("2143", Permutation(2, 1, 4, 3)), +] diff --git a/tests/tests_cli/test_cli.py b/tests/tests_cli/test_cli.py new file mode 100644 index 0000000..fa33a52 --- /dev/null +++ b/tests/tests_cli/test_cli.py @@ -0,0 +1,81 @@ +import pytest + +from tests.test_utils import _check_values +from symmetria.cli.cli import ( + _is_a_flag, + _is_a_permutation, +) +from symmetria.cli._commands import ( + _parse_permutation, + _execute_help_command, + _execute_error_message, + _execute_version_command, + _execute_permutation_command, +) +from tests.tests_cli.test_cases import ( + TEST_IS_A_FLAG, + TEST_IS_PERMUTATION, + TEST_PARSE_PERMUTATION, +) + + +@pytest.mark.parametrize( + argnames="command, expected_value", + argvalues=TEST_IS_A_FLAG, + ids=[f"_is_a_flag('{command}') = {b}" for command, b in TEST_IS_A_FLAG], +) +def test_is_a_flag(command, expected_value) -> None: + """Test method `_is_a_flag()`.""" + _check_values( + expression=f"_is_a_flag('{command}') = {expected_value}", + evaluation=_is_a_flag(command=command), + expected=expected_value, + ) + + +@pytest.mark.parametrize( + argnames="command, expected_value", + argvalues=TEST_IS_PERMUTATION, + ids=[f"_is_a_permutation('{command}') = {b}" for command, b in TEST_IS_PERMUTATION], +) +def test_is_a_permutation(command, expected_value) -> None: + """Test method `_is_a_permutation()`.""" + _check_values( + expression=f"_is_a_permutation('{command}')", + evaluation=_is_a_permutation(command=command), + expected=expected_value, + ) + + +@pytest.mark.parametrize( + argnames="permutation, expected_value", + argvalues=TEST_PARSE_PERMUTATION, + ids=[f"_parse_permutation('{p}') = {b}" for p, b in TEST_PARSE_PERMUTATION], +) +def test_parse_permutation(permutation, expected_value) -> None: + """Test method `_parse_permutation()`.""" + _check_values( + expression=f"_parse_permutation('{permutation}')", + evaluation=_parse_permutation(permutation=permutation), + expected=expected_value, + ) + + +def test_execute_version_command() -> None: + with pytest.raises(SystemExit): + _execute_version_command() + + +def test_execute_error_message() -> None: + with pytest.raises(SystemExit): + _execute_error_message("hello-world", 2) + + +def test_execute_help_command() -> None: + with pytest.raises(SystemExit): + _execute_help_command() + + +def test_permutation_command() -> None: + with pytest.raises(SystemExit): + _execute_permutation_command(permutation="123")