Skip to content

[WIP] New experimental argparse wrapper to have all options on all parsers #1812

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

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
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
66 changes: 66 additions & 0 deletions pythonforandroid/mergedargsparser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""
This provides a wrapper that allows multiple different ArgumentParser's
parse_args() function to return an argument object that will have an
attribute for ANY argument provided through ANY of all the parsers with
a value of None.

With this, we can avoid missing attribute errors if a code path was
called through a different initial toolchain command. These could also
be avoided through tons of hasattr()/getattr() calls, but that tends
to become unwieldy.
"""


class MergedArgsOverlay(object):
def __init__(self, original_args, all_args_names_set):
self._regular_args = original_args
self._all_possible_args = all_args_names_set

@property
def __dict__(self):
# Needed because argparse may for subparsers call our overridden
# parse_known_arguments() internally, then try to use vars()
# on this and return a new repackaged Namespace() based on it.
#
# In this case we just want to return the original objects args,
# since the "outside" parse_args/parse_known_args call will re-wrap
# this anyway, and otherwise we'd just confuse argparse internals
# with our additional None'd arguments.
return dict(vars(self._regular_args))

def __getattr__(self, name):
if not name.startswith("__") and hasattr(self._regular_args, name):
return getattr(self._regular_args, name)
if name in self._all_possible_args:
return None
raise AttributeError("no such attribute {}".format(name))


def wrap_as_parser_with_merged_args(args_store_set, parser):
original_parse_args = parser.parse_args
original_parse_known_args = parser.parse_known_args
original_add_argument = parser.add_argument

def parse_args_override(*args):
args_obj = original_parse_args(args)
while isinstance(args_obj, MergedArgsOverlay): # handle nested wraps
args_obj = MergedArgsOverlay._regular_args
return MergedArgsOverlay(args_obj, args_store_set)

def parse_known_args_override(*args):
args_obj, args_unknown = original_parse_known_args(*args)
while isinstance(args_obj, MergedArgsOverlay): # handle nested wraps
args_obj = MergedArgsOverlay._regular_args
return (MergedArgsOverlay(args_obj, args_store_set), args_unknown)

def add_argument_override(*args, **kwargs):
for arg in args:
while arg.startswith("-"):
arg = arg[1:]
args_store_set.add(arg.replace("-", "_"))
return original_add_argument(*args, **kwargs)

parser.parse_args = parse_args_override
parser.parse_known_args = parse_known_args_override
parser.add_argument = add_argument_override
return parser
52 changes: 35 additions & 17 deletions pythonforandroid/toolchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from __future__ import print_function
from os import environ
from pythonforandroid import __version__
from pythonforandroid.mergedargsparser import wrap_as_parser_with_merged_args
from pythonforandroid.pythonpackage import get_dep_names_of_package
from pythonforandroid.recommendations import (
RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API)
Expand Down Expand Up @@ -246,13 +247,28 @@ def __init__(self):
argv.append(argv.pop(1)) # the --color arg
argv.append(argv.pop(1)) # the --storage-dir arg

parser = NoAbbrevParser(
description='A packaging tool for turning Python scripts and apps '
'into Android APKs')
# This set keeps all args ever seen, so the parse_args() function of
# each parser can return 'None' even for arguments only truly provided
# by other command's parsers, making our life easier.
# (Otherwise we'd need to use hasattr()/getattr() a lot since many
# code paths are reachable with multiple different command parsers)
merged_args_store = set()

parser = wrap_as_parser_with_merged_args(
merged_args_store,
NoAbbrevParser(description=(
'A packaging tool for turning Python scripts and apps '
'into Android APKs'
))
)

generic_parser = argparse.ArgumentParser(
add_help=False,
description='Generic arguments applied to all commands')
generic_parser = wrap_as_parser_with_merged_args(
merged_args_store,
argparse.ArgumentParser(
add_help=False,
description='Generic arguments applied to all commands'
)
)
argparse.ArgumentParser(
add_help=False, description='Arguments for dist building')

Expand Down Expand Up @@ -390,7 +406,9 @@ def add_parser(subparsers, *args, **kwargs):
"""
if 'aliases' in kwargs and sys.version_info.major < 3:
kwargs.pop('aliases')
return subparsers.add_parser(*args, **kwargs)
return wrap_as_parser_with_merged_args(
merged_args_store, subparsers.add_parser(*args, **kwargs)
)

add_parser(
subparsers,
Expand Down Expand Up @@ -568,10 +586,10 @@ def add_parser(subparsers, *args, **kwargs):

args, unknown = parser.parse_known_args(sys.argv[1:])
args.unknown_args = unknown
if hasattr(args, "private") and args.private is not None:
if args.private is not None:
# Pass this value on to the internal bootstrap build.py:
args.unknown_args += ["--private", args.private]
if hasattr(args, "ignore_setup_py") and args.ignore_setup_py:
if args.ignore_setup_py:
args.use_setup_py = False

self.args = args
Expand All @@ -586,24 +604,24 @@ def add_parser(subparsers, *args, **kwargs):
logger.setLevel(logging.DEBUG)

self.ctx = Context()
self.ctx.use_setup_py = getattr(args, "use_setup_py", True)
self.ctx.use_setup_py = (
args.use_setup_py if args.use_setup_py is not None else True
)

have_setup_py_or_similar = False
if getattr(args, "private", None) is not None:
project_dir = getattr(args, "private")
if (os.path.exists(os.path.join(project_dir, "setup.py")) or
os.path.exists(os.path.join(project_dir,
if args.private is not None:
if (os.path.exists(os.path.join(args.private, "setup.py")) or
os.path.exists(os.path.join(args.private,
"pyproject.toml"))):
have_setup_py_or_similar = True

# Process requirements and put version in environ
if hasattr(args, 'requirements'):
if args.requirements is not None:
requirements = []

# Add dependencies from setup.py, but only if they are recipes
# (because otherwise, setup.py itself will install them later)
if (have_setup_py_or_similar and
getattr(args, "use_setup_py", False)):
if have_setup_py_or_similar and args.use_setup_py is True:
try:
info("Analyzing package dependencies. MAY TAKE A WHILE.")
# Get all the dependencies corresponding to a recipe:
Expand Down