Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Refactoring CLI argument parsing from docopt to argparse #646

Merged
merged 16 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ v3.7/
v3.8/
v3.9/
wheelhouse
ia.dist/
ia.bin
3 changes: 1 addition & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v5.0.0
hooks:
- id: check-builtin-literals
- id: check-executables-have-shebangs
Expand Down Expand Up @@ -47,7 +47,6 @@ repos:
- id: mypy
additional_dependencies:
- tqdm-stubs
- types-docopt
- types-jsonpatch
- types-requests
- types-setuptools
Expand Down
10 changes: 10 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
Release History
---------------

5.0.0 (2024-11-07)
++++++++++++++++++

**Features and Improvements**

- Updated the CLI's command-line argument parsing by replacing the obsolete ``docopt``
with the native ``argparse`` library, ensuring continued functionality
and future compatibility.
***Note: While the CLI functionality hasn't changed, some commands may need to be formatted slightly differently. If you encounter any issues, refer to ``ia --help`` and ``ia {command} --help`` if you run into any issues.***

4.1.0 (2024-05-07)
++++++++++++++++++

Expand Down
6 changes: 3 additions & 3 deletions internetarchive/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# The internetarchive module is a Python/CLI interface to Archive.org.
#
# Copyright (C) 2012-2019 Internet Archive
# Copyright (C) 2012-2024 Internet Archive
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand Down Expand Up @@ -29,14 +29,14 @@
>>> item.exists
True

:copyright: (C) 2012-2019 by Internet Archive.
:copyright: (C) 2012-2024 by Internet Archive.
:license: AGPL 3, see LICENSE for more details.
"""

__title__ = 'internetarchive'
__author__ = 'Jacob M. Johnson'
__license__ = 'AGPL 3'
__copyright__ = 'Copyright (C) 2012-2019 Internet Archive'
__copyright__ = 'Copyright (C) 2012-2024 Internet Archive'

from .__version__ import __version__ # isort:skip
from internetarchive.api import (
Expand Down
2 changes: 1 addition & 1 deletion internetarchive/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '4.1.0'
__version__ = '5.0.0'
4 changes: 2 additions & 2 deletions internetarchive/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# The internetarchive module is a Python/CLI interface to Archive.org.
#
# Copyright (C) 2012-2019 Internet Archive
# Copyright (C) 2012-2024 Internet Archive
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand All @@ -22,7 +22,7 @@

This module implements the Internetarchive API.

:copyright: (C) 2012-2019 by Internet Archive.
:copyright: (C) 2012-2024 by Internet Archive.
:license: AGPL 3, see LICENSE for more details.
"""
from __future__ import annotations
Expand Down
4 changes: 2 additions & 2 deletions internetarchive/auth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# The internetarchive module is a Python/CLI interface to Archive.org.
#
# Copyright (C) 2012-2019 Internet Archive
# Copyright (C) 2012-2024 Internet Archive
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand All @@ -22,7 +22,7 @@

This module contains the Archive.org authentication handlers for Requests.

:copyright: (C) 2012-2019 by Internet Archive.
:copyright: (C) 2012-2024 by Internet Archive.
:license: AGPL 3, see LICENSE for more details.
"""
from __future__ import annotations
Expand Down
4 changes: 2 additions & 2 deletions internetarchive/catalog.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# The internetarchive module is a Python/CLI interface to Archive.org.
#
# Copyright (C) 2012-2019 Internet Archive
# Copyright (C) 2012-2024 Internet Archive
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand All @@ -22,7 +22,7 @@

This module contains objects for interacting with the Archive.org catalog.

:copyright: (C) 2012-2019 by Internet Archive.
:copyright: (C) 2012-2024 by Internet Archive.
:license: AGPL 3, see LICENSE for more details.
"""
from __future__ import annotations
Expand Down
32 changes: 19 additions & 13 deletions internetarchive/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# The internetarchive module is a Python/CLI interface to Archive.org.
#
# Copyright (C) 2012-2016, 2021 Internet Archive
# Copyright (C) 2012-2024 Internet Archive
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand All @@ -20,31 +20,37 @@
internetarchive.cli
~~~~~~~~~~~~~~~~~~~

:copyright: (C) 2012-2016, 2021 by Internet Archive.
:copyright: (C) 2012-2024 by Internet Archive.
:license: AGPL 3, see LICENSE for more details.
"""
from internetarchive.cli import (
argparser,
cli_utils,
ia,
ia_configure,
ia_copy,
ia_delete,
ia_download,
ia_list,
ia_metadata,
ia_move,
ia_reviews,
ia_search,
ia_tasks,
ia_upload,
)

__all__ = [
'ia',
'ia_configure',
'ia_delete',
'ia_download',
'ia_list',
'ia_metadata',
'ia_search',
'ia_tasks',
'ia_upload',
'argparser',
"ia",
"cli_utils",
"ia_configure",
"ia_copy",
"ia_delete",
"ia_download",
"ia_list",
"ia_metadata",
"ia_move",
"ia_reviews",
"ia_search",
"ia_tasks",
"ia_upload",
]
77 changes: 0 additions & 77 deletions internetarchive/cli/argparser.py

This file was deleted.

137 changes: 137 additions & 0 deletions internetarchive/cli/cli_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""
interneratchive.cli.cli_utils

"""

# Copyright (C) 2012-2024 Internet Archive
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from __future__ import annotations

import argparse
import os
import signal
import sys
from collections import defaultdict
from typing import Mapping
from urllib.parse import parse_qsl

from internetarchive.utils import InvalidIdentifierException, validate_s3_identifier


def get_args_dict(args: list[str],
query_string: bool = False,
header: bool = False) -> dict:
args = args or []
if not isinstance(args, list):
args = [args]
metadata: dict[str, list | str] = defaultdict(list)
for md in args:
if query_string:
if (":" in md) and ("=" not in md):
md = md.replace(":", "=").replace(";", "&")
for key, value in parse_qsl(md):
assert value
metadata[key] = value
else:
key, value = md.split(":", 1)
assert value
if value not in metadata[key]:
metadata[key].append(value) # type: ignore

for key in metadata:
# Flatten single item lists.
if len(metadata[key]) <= 1:
metadata[key] = metadata[key][0]

return metadata


def get_args_header_dict(args: list[str]) -> dict:
h = get_args_dict(args)
return {k: v.strip() for k, v in h.items()}


def get_args_dict_many_write(metadata: Mapping):
changes: dict[str, dict] = defaultdict(dict)
for key, value in metadata.items():
target = "/".join(key.split("/")[:-1])
field = key.split("/")[-1]
if not changes[target]:
changes[target] = {field: value}
else:
changes[target][field] = value
return changes


def convert_str_list_to_unicode(str_list: list[bytes]):
encoding = sys.getfilesystemencoding()
return [b.decode(encoding) for b in str_list]


def validate_identifier(identifier):
try:
validate_s3_identifier(identifier)
except InvalidIdentifierException as e:
raise argparse.ArgumentTypeError(str(e))
return identifier


def prepare_args_dict(args, parser, arg_type="metadata", many=False, query_string=False):
if not args:
return {}
try:
if many:
return get_args_dict_many_write([item for sublist in args for item in sublist])
else:
if isinstance(args[0], list):
return get_args_dict([item for sublist in args for item in sublist],
query_string=True)
else:
return get_args_dict(args, query_string=True)
except ValueError as e:
parser.error(f"--{arg_type} must be formatted as --{arg_type}='key:value'")


def validate_dir_path(path):
"""
Check if the given path is a directory that exists.

Args:
path (str): The path to check.

Returns:
str: The validated directory path.

Raises:
argparse.ArgumentTypeError: If the path is not a valid directory.
"""
if os.path.isdir(path):
return path
else:
raise argparse.ArgumentTypeError(f"'{path}' is not a valid directory")


def exit_on_signal(sig, frame):
"""
Exit the program cleanly upon receiving a specified signal.

This function is designed to be used as a signal handler. When a signal
(such as SIGINT or SIGPIPE) is received, it exits the program with an
exit code of 128 plus the signal number. This convention helps to
distinguish between regular exit codes and those caused by signals.
"""
exit_code = 128 + sig
sys.exit(exit_code)
Loading