flet-cli 0.81.0.dev7148__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. flet_cli-0.81.0.dev7148/PKG-INFO +35 -0
  2. flet_cli-0.81.0.dev7148/README.md +17 -0
  3. flet_cli-0.81.0.dev7148/pyproject.toml +28 -0
  4. flet_cli-0.81.0.dev7148/setup.cfg +4 -0
  5. flet_cli-0.81.0.dev7148/src/flet_cli/__pyinstaller/__init__.py +5 -0
  6. flet_cli-0.81.0.dev7148/src/flet_cli/__pyinstaller/config.py +1 -0
  7. flet_cli-0.81.0.dev7148/src/flet_cli/__pyinstaller/hook-flet.py +9 -0
  8. flet_cli-0.81.0.dev7148/src/flet_cli/__pyinstaller/macos_utils.py +120 -0
  9. flet_cli-0.81.0.dev7148/src/flet_cli/__pyinstaller/rthooks/pyi_rth_localhost_fletd.py +9 -0
  10. flet_cli-0.81.0.dev7148/src/flet_cli/__pyinstaller/utils.py +25 -0
  11. flet_cli-0.81.0.dev7148/src/flet_cli/__pyinstaller/win_utils.py +109 -0
  12. flet_cli-0.81.0.dev7148/src/flet_cli/cli.py +119 -0
  13. flet_cli-0.81.0.dev7148/src/flet_cli/commands/base.py +91 -0
  14. flet_cli-0.81.0.dev7148/src/flet_cli/commands/build.py +136 -0
  15. flet_cli-0.81.0.dev7148/src/flet_cli/commands/build_base.py +1952 -0
  16. flet_cli-0.81.0.dev7148/src/flet_cli/commands/create.py +128 -0
  17. flet_cli-0.81.0.dev7148/src/flet_cli/commands/debug.py +156 -0
  18. flet_cli-0.81.0.dev7148/src/flet_cli/commands/devices.py +190 -0
  19. flet_cli-0.81.0.dev7148/src/flet_cli/commands/doctor.py +39 -0
  20. flet_cli-0.81.0.dev7148/src/flet_cli/commands/emulators.py +321 -0
  21. flet_cli-0.81.0.dev7148/src/flet_cli/commands/flutter_base.py +394 -0
  22. flet_cli-0.81.0.dev7148/src/flet_cli/commands/options.py +28 -0
  23. flet_cli-0.81.0.dev7148/src/flet_cli/commands/pack.py +328 -0
  24. flet_cli-0.81.0.dev7148/src/flet_cli/commands/publish.py +328 -0
  25. flet_cli-0.81.0.dev7148/src/flet_cli/commands/run.py +429 -0
  26. flet_cli-0.81.0.dev7148/src/flet_cli/commands/serve.py +76 -0
  27. flet_cli-0.81.0.dev7148/src/flet_cli/utils/android_sdk.py +285 -0
  28. flet_cli-0.81.0.dev7148/src/flet_cli/utils/distros.py +87 -0
  29. flet_cli-0.81.0.dev7148/src/flet_cli/utils/flutter.py +72 -0
  30. flet_cli-0.81.0.dev7148/src/flet_cli/utils/hash_stamp.py +22 -0
  31. flet_cli-0.81.0.dev7148/src/flet_cli/utils/jdk.py +134 -0
  32. flet_cli-0.81.0.dev7148/src/flet_cli/utils/merge.py +7 -0
  33. flet_cli-0.81.0.dev7148/src/flet_cli/utils/processes.py +70 -0
  34. flet_cli-0.81.0.dev7148/src/flet_cli/utils/project_dependencies.py +96 -0
  35. flet_cli-0.81.0.dev7148/src/flet_cli/utils/pyproject_toml.py +29 -0
  36. flet_cli-0.81.0.dev7148/src/flet_cli/version.py +1 -0
  37. flet_cli-0.81.0.dev7148/src/flet_cli.egg-info/PKG-INFO +35 -0
  38. flet_cli-0.81.0.dev7148/src/flet_cli.egg-info/SOURCES.txt +40 -0
  39. flet_cli-0.81.0.dev7148/src/flet_cli.egg-info/dependency_links.txt +1 -0
  40. flet_cli-0.81.0.dev7148/src/flet_cli.egg-info/entry_points.txt +2 -0
  41. flet_cli-0.81.0.dev7148/src/flet_cli.egg-info/requires.txt +8 -0
  42. flet_cli-0.81.0.dev7148/src/flet_cli.egg-info/top_level.txt +1 -0
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: flet-cli
3
+ Version: 0.81.0.dev7148
4
+ Summary: Flet CLI
5
+ Author-email: "Appveyor Systems Inc." <hello@flet.dev>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://flet.dev
8
+ Project-URL: Repository, https://github.com/flet-dev/flet
9
+ Project-URL: Documentation, https://flet.dev/docs
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: flet
13
+ Requires-Dist: watchdog>=4.0.0
14
+ Requires-Dist: packaging>=25.0
15
+ Requires-Dist: qrcode>=7.4.2
16
+ Requires-Dist: tomli>=1.1.0; python_version < "3.11"
17
+ Requires-Dist: cookiecutter>=2.6.0
18
+
19
+ # Flet CLI
20
+
21
+ Flet CLI is a command-line interface tool for Flet, a framework for building interactive multi-platform applications using Python.
22
+
23
+ ## Features
24
+
25
+ - Create new Flet projects
26
+ - Run Flet applications
27
+ - Package and deploy Flet apps
28
+
29
+ ## Basic Usage
30
+
31
+ To create a new Flet project:
32
+
33
+ ```
34
+ flet create myapp
35
+ ```
@@ -0,0 +1,17 @@
1
+ # Flet CLI
2
+
3
+ Flet CLI is a command-line interface tool for Flet, a framework for building interactive multi-platform applications using Python.
4
+
5
+ ## Features
6
+
7
+ - Create new Flet projects
8
+ - Run Flet applications
9
+ - Package and deploy Flet apps
10
+
11
+ ## Basic Usage
12
+
13
+ To create a new Flet project:
14
+
15
+ ```
16
+ flet create myapp
17
+ ```
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "flet-cli"
3
+ version = "0.81.0.dev7148"
4
+ description = "Flet CLI"
5
+ authors = [{ name = "Appveyor Systems Inc.", email = "hello@flet.dev" }]
6
+ license = "Apache-2.0"
7
+ readme = "README.md"
8
+ requires-python = ">=3.10"
9
+ dependencies = [
10
+ "flet",
11
+ "watchdog >=4.0.0",
12
+ "packaging >=25.0",
13
+ "qrcode >=7.4.2",
14
+ "tomli >= 1.1.0 ; python_version < '3.11'",
15
+ "cookiecutter >=2.6.0"
16
+ ]
17
+
18
+ [project.urls]
19
+ Homepage = "https://flet.dev"
20
+ Repository = "https://github.com/flet-dev/flet"
21
+ Documentation = "https://flet.dev/docs"
22
+
23
+ [project.entry-points."pyinstaller40"]
24
+ hook-dirs = "flet_cli.__pyinstaller:get_hook_dirs"
25
+
26
+ [build-system]
27
+ requires = ["setuptools"]
28
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ import os
2
+
3
+
4
+ def get_hook_dirs():
5
+ return [os.path.dirname(__file__)]
@@ -0,0 +1 @@
1
+ temp_bin_dir = None
@@ -0,0 +1,9 @@
1
+ import flet_cli.__pyinstaller.config as hook_config
2
+ from flet_cli.__pyinstaller.utils import get_flet_bin_path
3
+
4
+ bin_path = hook_config.temp_bin_dir
5
+ if not bin_path:
6
+ bin_path = get_flet_bin_path()
7
+
8
+ if bin_path:
9
+ datas = [(bin_path, "flet_desktop/app")]
@@ -0,0 +1,120 @@
1
+ import os
2
+ import plistlib
3
+ import shutil
4
+ import subprocess
5
+ import tarfile
6
+ from pathlib import Path
7
+
8
+ from flet.utils import safe_tar_extractall
9
+ from PyInstaller.building.icon import normalize_icon_type
10
+
11
+
12
+ def unpack_app_bundle(tar_path):
13
+ bin_dir = str(Path(tar_path).parent)
14
+
15
+ with tarfile.open(tar_path, "r:gz") as tar_arch:
16
+ safe_tar_extractall(tar_arch, bin_dir)
17
+ os.remove(tar_path)
18
+
19
+ return os.path.join(bin_dir, "Flet.app")
20
+
21
+
22
+ def update_flet_view_icon(app_path, icon_path):
23
+ print("Updating Flet View icon", app_path, icon_path)
24
+
25
+ icon_file = "AppIcon.icns"
26
+
27
+ # normalize icon
28
+ normalized_icon_path = normalize_icon_type(
29
+ icon_path, ("icns",), "icns", os.getcwd()
30
+ )
31
+
32
+ # patch icon
33
+ print("Copying icons from", normalized_icon_path)
34
+ shutil.copy(
35
+ normalized_icon_path,
36
+ os.path.join(app_path, "Contents", "Resources", icon_file),
37
+ )
38
+
39
+ # update icon file name
40
+ pl = __load_info_plist(app_path)
41
+ pl["CFBundleIconFile"] = icon_file
42
+ del pl["CFBundleIconName"]
43
+ __save_info_plist(app_path, pl)
44
+
45
+
46
+ def update_flet_view_version_info(
47
+ app_path,
48
+ bundle_id,
49
+ product_name,
50
+ product_version,
51
+ copyright,
52
+ ):
53
+ print("Updating Flet View plist", app_path)
54
+
55
+ pl = __load_info_plist(app_path)
56
+
57
+ if bundle_id:
58
+ pl["CFBundleIdentifier"] = bundle_id
59
+ if product_name:
60
+ pl["CFBundleName"] = product_name
61
+ pl["CFBundleDisplayName"] = product_name
62
+
63
+ # rename app bundle
64
+ new_app_path = os.path.join(Path(app_path).parent, f"{product_name}.app")
65
+ os.rename(app_path, new_app_path)
66
+ app_path = new_app_path
67
+ if product_version:
68
+ pl["CFBundleShortVersionString"] = product_version
69
+ if copyright:
70
+ pl["NSHumanReadableCopyright"] = copyright
71
+
72
+ __save_info_plist(app_path, pl)
73
+
74
+ return app_path
75
+
76
+
77
+ def assemble_app_bundle(app_path, tar_path):
78
+ # sign app bundle
79
+ print(f"Signing file {app_path}")
80
+ cmd_args = [
81
+ "codesign",
82
+ "-s",
83
+ "-",
84
+ "--force",
85
+ "--all-architectures",
86
+ "--timestamp",
87
+ "--deep",
88
+ app_path,
89
+ ]
90
+ p = subprocess.run(
91
+ cmd_args,
92
+ stdout=subprocess.PIPE,
93
+ stderr=subprocess.STDOUT,
94
+ universal_newlines=True,
95
+ )
96
+ if p.returncode:
97
+ raise SystemError(
98
+ f"codesign command ({cmd_args}) failed with error code {p.returncode}!\noutput: {p.stdout}"
99
+ )
100
+
101
+ # pack tar
102
+ with tarfile.open(tar_path, "w:gz") as tar:
103
+ tar.add(app_path, arcname=os.path.basename(app_path))
104
+
105
+ # cleanup
106
+ shutil.rmtree(app_path, ignore_errors=True)
107
+
108
+
109
+ def __load_info_plist(app_path):
110
+ with open(__get_plist_path(app_path), "rb") as fp:
111
+ return plistlib.load(fp)
112
+
113
+
114
+ def __save_info_plist(app_path, pl):
115
+ with open(__get_plist_path(app_path), "wb") as fp:
116
+ plistlib.dump(pl, fp)
117
+
118
+
119
+ def __get_plist_path(app_path):
120
+ return os.path.join(app_path, "Contents", "Info.plist")
@@ -0,0 +1,9 @@
1
+ import logging
2
+ import os
3
+
4
+ logger = logging.getLogger("flet")
5
+
6
+
7
+ logger.info("Running PyInstaller runtime hook for Flet...")
8
+
9
+ os.environ["FLET_SERVER_IP"] = "127.0.0.1"
@@ -0,0 +1,25 @@
1
+ import os
2
+ import tempfile
3
+ import uuid
4
+ from pathlib import Path
5
+
6
+ from flet.utils import copy_tree
7
+ from flet_desktop import get_package_bin_dir
8
+
9
+
10
+ def get_flet_bin_path():
11
+ bin_path = get_package_bin_dir()
12
+ if not os.path.exists(bin_path):
13
+ return None
14
+ return bin_path
15
+
16
+
17
+ def copy_flet_bin():
18
+ bin_path = get_flet_bin_path()
19
+ if not bin_path:
20
+ return None
21
+
22
+ # create temp bin dir
23
+ temp_bin_dir = Path(tempfile.gettempdir()).joinpath(str(uuid.uuid4()))
24
+ copy_tree(bin_path, str(temp_bin_dir))
25
+ return str(temp_bin_dir)
@@ -0,0 +1,109 @@
1
+ import os
2
+ import tempfile
3
+ import uuid
4
+ from pathlib import Path
5
+
6
+ import pefile
7
+ from packaging import version
8
+ from PyInstaller.building.icon import normalize_icon_type
9
+ from PyInstaller.compat import win32api
10
+ from PyInstaller.utils.win32 import versioninfo
11
+ from PyInstaller.utils.win32.icon import IconFile, normalize_icon_type
12
+
13
+
14
+ def update_flet_view_icon(exe_path, icon_path):
15
+ print("Updating Flet View icon", exe_path, icon_path)
16
+
17
+ RT_ICON = 3
18
+ RT_GROUP_ICON = 14
19
+
20
+ normalized_icon_path = normalize_icon_type(
21
+ icon_path, ("exe", "ico"), "ico", os.getcwd()
22
+ )
23
+ icon = IconFile(normalized_icon_path)
24
+ print("Copying icons from", normalized_icon_path)
25
+
26
+ hdst = win32api.BeginUpdateResource(exe_path, 0)
27
+
28
+ iconid = 1
29
+ # Each step in the following enumerate() will instantiate an IconFile object, as a result of deferred execution
30
+ # of the map() above.
31
+ i = 101
32
+ data = icon.grp_icon_dir()
33
+ data = data + icon.grp_icondir_entries(iconid)
34
+ win32api.UpdateResource(hdst, RT_GROUP_ICON, i, data, 1033)
35
+ print("Writing RT_GROUP_ICON %d resource with %d bytes", i, len(data))
36
+ for data in icon.images:
37
+ win32api.UpdateResource(hdst, RT_ICON, iconid, data, 1033)
38
+ print("Writing RT_ICON %d resource with %d bytes", iconid, len(data))
39
+ iconid = iconid + 1
40
+
41
+ win32api.EndUpdateResource(hdst, 0)
42
+
43
+
44
+ def update_flet_view_version_info(
45
+ exe_path,
46
+ product_name,
47
+ file_description,
48
+ product_version,
49
+ file_version,
50
+ company_name,
51
+ copyright,
52
+ ):
53
+ print("Updating Flet View version info", exe_path)
54
+
55
+ # load versioninfo from exe
56
+ if versioninfo.read_version_info_from_executable:
57
+ vs = versioninfo.read_version_info_from_executable(exe_path)
58
+ else:
59
+ vs = versioninfo.decode(exe_path)
60
+
61
+ # update file version
62
+ if file_version:
63
+ pv = version.parse(file_version)
64
+ filevers = (pv.major, pv.minor, pv.micro, 0)
65
+ vs.ffi.fileVersionMS = (filevers[0] << 16) | (filevers[1] & 0xFFFF)
66
+ vs.ffi.fileVersionLS = (filevers[2] << 16) | (filevers[3] & 0xFFFF)
67
+
68
+ # update string props
69
+ for k in vs.kids[0].kids[0].kids:
70
+ if k.name == "ProductName":
71
+ k.val = product_name if product_name else ""
72
+ elif k.name == "FileDescription":
73
+ k.val = file_description if file_description else ""
74
+ if k.name == "ProductVersion":
75
+ k.val = product_version if product_version else ""
76
+ if k.name == "FileVersion" and file_version:
77
+ k.val = file_version if file_version else ""
78
+ if k.name == "CompanyName":
79
+ k.val = company_name if company_name else ""
80
+ if k.name == "LegalCopyright":
81
+ k.val = copyright if copyright else ""
82
+
83
+ version_info_path = str(Path(tempfile.gettempdir()).joinpath(str(uuid.uuid4())))
84
+ with open(version_info_path, "w", encoding="utf-8") as f:
85
+ f.write(str(vs))
86
+
87
+ # Remember overlay
88
+ pe = pefile.PE(exe_path, fast_load=True)
89
+ overlay_before = pe.get_overlay()
90
+ pe.close()
91
+
92
+ hdst = win32api.BeginUpdateResource(exe_path, 0)
93
+ win32api.UpdateResource(
94
+ hdst, pefile.RESOURCE_TYPE["RT_VERSION"], 1, vs.toRaw(), 1033
95
+ )
96
+ win32api.EndUpdateResource(hdst, 0)
97
+
98
+ if overlay_before:
99
+ # Check if the overlay is still present
100
+ pe = pefile.PE(exe_path, fast_load=True)
101
+ overlay_after = pe.get_overlay()
102
+ pe.close()
103
+
104
+ # If the update removed the overlay data, re-append it
105
+ if not overlay_after:
106
+ with open(exe_path, "ab", encoding="utf-8") as exef:
107
+ exef.write(overlay_before)
108
+
109
+ return version_info_path
@@ -0,0 +1,119 @@
1
+ import argparse
2
+ import sys
3
+
4
+ import flet.version
5
+ import flet_cli.commands.build
6
+ import flet_cli.commands.create
7
+ import flet_cli.commands.debug
8
+ import flet_cli.commands.devices
9
+ import flet_cli.commands.doctor
10
+ import flet_cli.commands.emulators
11
+ import flet_cli.commands.pack
12
+ import flet_cli.commands.publish
13
+ import flet_cli.commands.run
14
+ import flet_cli.commands.serve
15
+
16
+
17
+ # Source https://stackoverflow.com/a/26379693
18
+ def set_default_subparser(
19
+ parser: argparse.ArgumentParser, name: str, args: list = None, index: int = 0
20
+ ):
21
+ """
22
+ Set a default subparser when no subparser is provided.
23
+ This should be called after setting up the argument parser but before
24
+ `parse_args()`.
25
+
26
+ Args:
27
+ name: The name of the default subparser to use.
28
+ args: A list of arguments passed to `parse_args()`.
29
+ index: Position in `sys.argv` where the default subparser should be
30
+ inserted.
31
+ """
32
+
33
+ # exit if help or version flags are present
34
+ if any(flag in sys.argv[1:] for flag in {"-h", "--help", "-V", "--version"}):
35
+ return
36
+
37
+ # all subparser actions
38
+ subparser_actions = [
39
+ action
40
+ for action in parser._subparsers._actions
41
+ if isinstance(action, argparse._SubParsersAction)
42
+ ]
43
+
44
+ # all subparser names
45
+ subparser_names = [
46
+ sp_name for action in subparser_actions for sp_name in action._name_parser_map
47
+ ]
48
+
49
+ # if an existing subparser is provided, skip setting a default
50
+ if any(arg in subparser_names for arg in sys.argv[1:]):
51
+ return
52
+
53
+ # if the default subparser doesn't exist, register it in the first subparser action
54
+ if (name not in subparser_names) and subparser_actions:
55
+ subparser_actions[0].add_parser(name)
56
+
57
+ # insert the default subparser into the appropriate argument list
58
+ if args is None:
59
+ if len(sys.argv) > 1:
60
+ sys.argv.insert(index, name)
61
+ else:
62
+ args.insert(index, name)
63
+
64
+
65
+ def get_parser() -> argparse.ArgumentParser:
66
+ """Construct and return the CLI argument parser."""
67
+ parser = argparse.ArgumentParser(
68
+ formatter_class=argparse.RawDescriptionHelpFormatter
69
+ )
70
+
71
+ # add version flag
72
+ parser.add_argument(
73
+ "--version",
74
+ "-V",
75
+ action="version",
76
+ version=(
77
+ f"Flet: {flet.version.flet_version}\n"
78
+ f"Flutter: {flet.version.flutter_version}\n"
79
+ f"Pyodide: {flet.version.pyodide_version}"
80
+ ),
81
+ )
82
+
83
+ sp = parser.add_subparsers(dest="command")
84
+
85
+ # register subcommands
86
+ flet_cli.commands.create.Command.register_to(sp, "create")
87
+ flet_cli.commands.run.Command.register_to(sp, "run")
88
+ flet_cli.commands.build.Command.register_to(sp, "build")
89
+ flet_cli.commands.debug.Command.register_to(sp, "debug")
90
+ flet_cli.commands.pack.Command.register_to(sp, "pack")
91
+ flet_cli.commands.publish.Command.register_to(sp, "publish")
92
+ flet_cli.commands.serve.Command.register_to(sp, "serve")
93
+ flet_cli.commands.emulators.Command.register_to(sp, "emulators")
94
+ flet_cli.commands.devices.Command.register_to(sp, "devices")
95
+ flet_cli.commands.doctor.Command.register_to(sp, "doctor")
96
+
97
+ # set "run" as the default subparser
98
+ set_default_subparser(parser, name="run", index=1)
99
+
100
+ return parser
101
+
102
+
103
+ def main():
104
+ parser = get_parser()
105
+
106
+ # print usage/help if called without arguments
107
+ if len(sys.argv) == 1:
108
+ parser.print_help(sys.stdout)
109
+ sys.exit(1)
110
+
111
+ # parse arguments
112
+ args = parser.parse_args()
113
+
114
+ # execute command
115
+ args.handler(args)
116
+
117
+
118
+ if __name__ == "__main__":
119
+ main()
@@ -0,0 +1,91 @@
1
+ import argparse
2
+ from typing import Any, Optional
3
+
4
+ from flet_cli.commands.options import Option, verbose_option
5
+
6
+
7
+ class CustomArgumentDefaultsHelpFormatter(argparse.HelpFormatter):
8
+ """
9
+ An argparse help formatter that appends default values to help text
10
+ selectively.
11
+
12
+ Defaults are added only when they are informative and not already
13
+ present in the help string. Noisy or redundant defaults (such as
14
+ None, empty lists, booleans for flag arguments, or suppressed values)
15
+ are omitted.
16
+ """
17
+
18
+ def _get_help_string(self, action: argparse.Action) -> str:
19
+ help_text = action.help or ""
20
+ default = action.default
21
+
22
+ # skip appending a default
23
+ if (
24
+ default is None
25
+ or default == []
26
+ or isinstance(default, bool) # store_true / store_false flags
27
+ or default is argparse.SUPPRESS
28
+ or any(token in help_text for token in ("%(default)", "(default:"))
29
+ ):
30
+ return help_text
31
+
32
+ # only add defaults for optionals or for nargs implying optional values
33
+ defaulting_nargs = (argparse.OPTIONAL, argparse.ZERO_OR_MORE)
34
+ if action.option_strings or action.nargs in defaulting_nargs:
35
+ help_text += " (default: %(default)s)"
36
+
37
+ return help_text
38
+
39
+
40
+ class BaseCommand:
41
+ """A CLI subcommand"""
42
+
43
+ # The subcommand's name
44
+ name: Optional[str] = None
45
+ # The subcommand's help string, if not given, __doc__ will be used.
46
+ description: Optional[str] = None
47
+ # A list of pre-defined options which will be loaded on initializing
48
+ # Rewrite this if you don't want the default ones
49
+ arguments: list[Option] = [verbose_option]
50
+
51
+ def __init__(self, parser: argparse.ArgumentParser) -> None:
52
+ for arg in self.arguments:
53
+ arg.add_to_parser(parser)
54
+ self.add_arguments(parser)
55
+
56
+ @classmethod
57
+ def register_to(
58
+ cls,
59
+ subparsers: argparse._SubParsersAction,
60
+ name: Optional[str] = None,
61
+ **kwargs: Any,
62
+ ) -> None:
63
+ """Register a subcommand to the subparsers,
64
+ with an optional name of the subcommand.
65
+ """
66
+ help_text = cls.description or cls.__doc__
67
+ name = name or cls.name or ""
68
+
69
+ # Remove the existing subparser as it will raise an error on Python 3.11+
70
+ subparsers._name_parser_map.pop(name, None)
71
+ subactions = subparsers._get_subactions()
72
+ subactions[:] = [action for action in subactions if action.dest != name]
73
+ parser = subparsers.add_parser(
74
+ name,
75
+ description=help_text,
76
+ help=help_text,
77
+ formatter_class=CustomArgumentDefaultsHelpFormatter,
78
+ **kwargs,
79
+ )
80
+ command = cls(parser)
81
+ parser.set_defaults(handler=command.handle)
82
+
83
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
84
+ """Manipulate the argument parser to add more arguments"""
85
+ pass
86
+
87
+ def handle(self, options: argparse.Namespace) -> None:
88
+ """The command handler function.
89
+ :param options: the parsed Namespace object
90
+ """
91
+ raise NotImplementedError