From 847c3a9704e922175fd1676a56abc86fdce9508e Mon Sep 17 00:00:00 2001 From: Artur Barseghyan Date: Fri, 23 Jun 2023 00:30:41 +0200 Subject: [PATCH] Better error handling in CLI --- CHANGELOG.rst | 8 ++ docs/cli.rst | 16 ++- setup.py | 6 +- src/faker_file/__init__.py | 2 +- src/faker_file/cli/__init__.py | 0 src/faker_file/cli/command.py | 91 ++++++++++++++++ src/faker_file/{cli.py => cli/helpers.py} | 126 +++++----------------- src/faker_file/tests/test_cli.py | 14 ++- 8 files changed, 156 insertions(+), 107 deletions(-) create mode 100644 src/faker_file/cli/__init__.py create mode 100644 src/faker_file/cli/command.py rename src/faker_file/{cli.py => cli/helpers.py} (62%) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 30b44c2..49c16e8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,8 +15,16 @@ are used for versioning (schema follows below): 0.3.4 to 0.4). - All backwards incompatible changes are mentioned in this document. +0.16.1 +------ +2023-06-23 + +- Better error handling in CLI. + 0.16 ---- +2023-06-21 + .. note:: This release is dedicated to my beloved son - Tigran, who turned 11! diff --git a/docs/cli.rst b/docs/cli.rst index e0bb0dc..dc9f5d9 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -1,14 +1,26 @@ CLI === +.. External references + +.. _pipx: https://pypa.github.io/pipx/ + It's possible to generate files from CLI. .. note:: - For using CLI you should install all common dependencies: + For using CLI you should install all common dependencies. + +Install using `pipx`_ (recommended): + +.. code-block:: sh + + pipx install faker-file[common] + +Install using ``pip``. .. code-block:: sh - pip install faker-file[common] + pip install faker-file[common] --user List available provider options ------------------------------- diff --git a/setup.py b/setup.py index 12f6ac3..96c6491 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup -version = "0.16" +version = "0.16.1" try: readme = open(os.path.join(os.path.dirname(__file__), "README.rst")).read() @@ -127,7 +127,9 @@ url="https://github.com/barseghyanartur/faker-file/", package_dir={"": "src"}, packages=find_packages(where="./src"), - entry_points={"console_scripts": ["faker-file = faker_file.cli:main"]}, + entry_points={ + "console_scripts": ["faker-file = faker_file.cli.command:main"] + }, license="MIT", python_requires=">=3.7", install_requires=install_requires, diff --git a/src/faker_file/__init__.py b/src/faker_file/__init__.py index 9d8b838..348c992 100644 --- a/src/faker_file/__init__.py +++ b/src/faker_file/__init__.py @@ -1,5 +1,5 @@ __title__ = "faker_file" -__version__ = "0.16" +__version__ = "0.16.1" __author__ = "Artur Barseghyan " __copyright__ = "2022-2023 Artur Barseghyan" __license__ = "MIT" diff --git a/src/faker_file/cli/__init__.py b/src/faker_file/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/faker_file/cli/command.py b/src/faker_file/cli/command.py new file mode 100644 index 0000000..704fc49 --- /dev/null +++ b/src/faker_file/cli/command.py @@ -0,0 +1,91 @@ +import argparse +import sys +import typing + +from .. import __version__ + +__author__ = "Artur Barseghyan " +__copyright__ = "2023 Artur Barseghyan" +__license__ = "MIT" +__all__ = ("main",) + + +def main(): + try: + from .helpers import ( + PROVIDERS, + generate_completion_file, + generate_file, + get_method_kwargs, + is_optional_type, + ) + except ImportError: + print("You need to pip install faker-file[common] to use the CLI") + sys.exit(1) + + parser = argparse.ArgumentParser( + description="CLI for the faker-file package." + ) + subparsers = parser.add_subparsers( + dest="command", help="Available file providers." + ) + + # Add generate-completion subparser + __generate_completion_subparser = subparsers.add_parser( + "generate-completion", + help="Generate bash completion file.", + ) + + # Add version subparser + __version_subparser = subparsers.add_parser( + "version", + help="Print version.", + ) + + for method_name, provider in PROVIDERS.items(): + subparser = subparsers.add_parser( + method_name, + help=f"Generate a {method_name.split('_file')[0]} file.", + ) + method_kwargs, annotations = get_method_kwargs(provider, method_name) + for arg, default in method_kwargs.items(): + arg_type = annotations[arg] + arg_kwargs = { + "default": default, + "help": f"{arg} (default: {default})", + "type": ( + arg_type.__args__[0] + if isinstance(arg_type, typing._GenericAlias) + and is_optional_type(arg_type) + else arg_type + ), + } + + subparser.add_argument(f"--{arg}", **arg_kwargs) + + # Add the optional num_files argument + subparser.add_argument( + "--nb_files", + default=1, + type=int, + help="number of files to generate (default: 1)", + ) + + args = parser.parse_args() + + if args.command == "generate-completion": + generate_completion_file() + elif args.command == "version": + print(__version__) + elif args.command: + kwargs = {k: v for k, v in vars(args).items() if k not in ("command",)} + for counter in range(args.nb_files): + output_file = generate_file(args.command, **kwargs) + print( + f"Generated {args.command} file " + f"({counter+1} of {args.nb_files}): " + f"{output_file.data['filename']}" + ) + else: + parser.print_help() + sys.exit(1) diff --git a/src/faker_file/cli.py b/src/faker_file/cli/helpers.py similarity index 62% rename from src/faker_file/cli.py rename to src/faker_file/cli/helpers.py index 55ff488..61ea0cd 100644 --- a/src/faker_file/cli.py +++ b/src/faker_file/cli/helpers.py @@ -1,49 +1,46 @@ -import argparse import inspect import os -import sys import typing from copy import deepcopy from typing import Any, Dict, Tuple, Type from faker import Faker -from . import __version__ -from .base import FileMixin, StringValue -from .providers.bin_file import BinFileProvider -from .providers.csv_file import CsvFileProvider -from .providers.docx_file import DocxFileProvider -from .providers.eml_file import EmlFileProvider -from .providers.epub_file import EpubFileProvider -from .providers.generic_file import GenericFileProvider -from .providers.ico_file import IcoFileProvider -from .providers.jpeg_file import JpegFileProvider -from .providers.mp3_file import Mp3FileProvider -from .providers.odp_file import OdpFileProvider -from .providers.ods_file import OdsFileProvider -from .providers.odt_file import OdtFileProvider -from .providers.pdf_file import PdfFileProvider -from .providers.png_file import PngFileProvider -from .providers.pptx_file import PptxFileProvider -from .providers.rtf_file import RtfFileProvider -from .providers.svg_file import SvgFileProvider -from .providers.tar_file import TarFileProvider -from .providers.txt_file import TxtFileProvider -from .providers.webp_file import WebpFileProvider -from .providers.xlsx_file import XlsxFileProvider -from .providers.xml_file import XmlFileProvider -from .providers.zip_file import ZipFileProvider +from ..base import FileMixin, StringValue +from ..providers.bin_file import BinFileProvider +from ..providers.csv_file import CsvFileProvider +from ..providers.docx_file import DocxFileProvider +from ..providers.eml_file import EmlFileProvider +from ..providers.epub_file import EpubFileProvider +from ..providers.generic_file import GenericFileProvider +from ..providers.ico_file import IcoFileProvider +from ..providers.jpeg_file import JpegFileProvider +from ..providers.mp3_file import Mp3FileProvider +from ..providers.odp_file import OdpFileProvider +from ..providers.ods_file import OdsFileProvider +from ..providers.odt_file import OdtFileProvider +from ..providers.pdf_file import PdfFileProvider +from ..providers.png_file import PngFileProvider +from ..providers.pptx_file import PptxFileProvider +from ..providers.rtf_file import RtfFileProvider +from ..providers.svg_file import SvgFileProvider +from ..providers.tar_file import TarFileProvider +from ..providers.txt_file import TxtFileProvider +from ..providers.webp_file import WebpFileProvider +from ..providers.xlsx_file import XlsxFileProvider +from ..providers.xml_file import XmlFileProvider +from ..providers.zip_file import ZipFileProvider __author__ = "Artur Barseghyan " __copyright__ = "2023 Artur Barseghyan" __license__ = "MIT" -__all__ = [ +__all__ = ( "generate_completion_file", "generate_file", "get_method_kwargs", "is_optional_type", - "main", -] + "PROVIDERS", +) KWARGS_DROP = { "self", # Drop as irrelevant @@ -212,72 +209,3 @@ def generate_completion_file(): f.write(completion_script) print(f"Generated bash completion file: {file_path}") - - -def main(): - parser = argparse.ArgumentParser( - description="CLI for the faker-file package." - ) - subparsers = parser.add_subparsers( - dest="command", help="Available file providers." - ) - - # Add generate-completion subparser - __generate_completion_subparser = subparsers.add_parser( - "generate-completion", - help="Generate bash completion file.", - ) - - # Add version subparser - __version_subparser = subparsers.add_parser( - "version", - help="Print version.", - ) - - for method_name, provider in PROVIDERS.items(): - subparser = subparsers.add_parser( - method_name, - help=f"Generate a {method_name.split('_file')[0]} file.", - ) - method_kwargs, annotations = get_method_kwargs(provider, method_name) - for arg, default in method_kwargs.items(): - arg_type = annotations[arg] - arg_kwargs = { - "default": default, - "help": f"{arg} (default: {default})", - "type": ( - arg_type.__args__[0] - if isinstance(arg_type, typing._GenericAlias) - and is_optional_type(arg_type) - else arg_type - ), - } - - subparser.add_argument(f"--{arg}", **arg_kwargs) - - # Add the optional num_files argument - subparser.add_argument( - "--nb_files", - default=1, - type=int, - help="number of files to generate (default: 1)", - ) - - args = parser.parse_args() - - if args.command == "generate-completion": - generate_completion_file() - elif args.command == "version": - print(__version__) - elif args.command: - kwargs = {k: v for k, v in vars(args).items() if k not in ("command",)} - for counter in range(args.nb_files): - output_file = generate_file(args.command, **kwargs) - print( - f"Generated {args.command} file " - f"({counter+1} of {args.nb_files}): " - f"{output_file.data['filename']}" - ) - else: - parser.print_help() - sys.exit(1) diff --git a/src/faker_file/tests/test_cli.py b/src/faker_file/tests/test_cli.py index f929de9..e9bbd6c 100644 --- a/src/faker_file/tests/test_cli.py +++ b/src/faker_file/tests/test_cli.py @@ -2,9 +2,12 @@ import re import subprocess import unittest +from importlib import import_module, reload from parametrize import parametrize +from ..cli.command import main + __author__ = "Artur Barseghyan " __copyright__ = "2022-2023 Artur Barseghyan" __license__ = "MIT" @@ -28,9 +31,6 @@ def convert_value_to_cli_arg(value) -> str: class TestCLI(unittest.TestCase): """CLI tests.""" - def setUp(self): - """Set up.""" - @parametrize( "method_name, kwargs", [ @@ -265,3 +265,11 @@ def test_cli_version(self: "TestCLI") -> None: cmd = ["faker-file", "version"] res = subprocess.check_output(cmd).strip() self.assertTrue(VERSION_PATTERN.match(res.decode())) + + def test_broken_imports(self: "TestCLI") -> None: + """Test broken imports.""" + _module = import_module("faker_file.cli.helpers") + del _module.__dict__["PROVIDERS"] + with self.assertRaises(SystemExit): + main() + reload(_module)