Skip to content

Commit

Permalink
Positional arguments kind of possible. Some gotchas as the subcommand
Browse files Browse the repository at this point in the history
context parameters is not implemented, so the order is the same for all
and ordering is based naively on what's been given in the configuration.

V0.5.0-v0.5.0 bump version to 0.5.1

Included python-fire inside the package directory until
python-poetry/poetry#2427 gets merged and a
submodule can be used instead
  • Loading branch information
jonion-tears committed May 25, 2020
1 parent 5679c93 commit c4c43bb
Show file tree
Hide file tree
Showing 32 changed files with 2,426 additions and 106 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ clima.egg-info/
*.html
.cache
**/*.log
.hypothesis
4 changes: 0 additions & 4 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +0,0 @@
[submodule "python-fire"]
path = python-fire
url = https://github.com/d3rp/python-fire.git
branch = clima
6 changes: 3 additions & 3 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ verify_ssl = true
name = "pypi"

[packages]
fire = {path = "python-fire"}
bleach = ">=3.1.4"
bleach = ">=3.1.5"
pytest-watch = "==4.*,>=4.2.0"
tabulate = "==0.*,>=0.8.7"

[dev-packages]
pytest = "*"
Expand All @@ -16,4 +16,4 @@ mock = "*"
python-levenshtein = "*"

[requires]
python_version = "3.8"
python_version = "3.7"
2 changes: 1 addition & 1 deletion clima/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

from clima.core import c, Schema

__version__ = '0.4.3'
__version__ = '0.5.1'
36 changes: 31 additions & 5 deletions clima/core.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from collections import ChainMap
import inspect
import sys
import os
from functools import partial

from fire import Fire
from clima.fire import Fire

from clima import schema, utils
from clima import docstring, configfile
Expand Down Expand Up @@ -165,12 +166,37 @@ def prepare_signatures(cls, nt):
k: v for k, v in cls.__dict__.items()
if not k.startswith('_') and inspect.isfunction(v)
}

params = [
inspect.Parameter(name=field, kind=inspect._VAR_KEYWORD)
for field in nt._fields
]

# pop the post_init from the end of parameters
params.pop()

# Hacking sys.argv to include positional keywords with assumed keyword names
# as I couldn't find another workaround to tell python-fire how to parse these
# The alternative had been to integrate python-fire with tighter coupling into this
if len(sys.argv) > 2 and not any(['-h' in sys.argv, '--help' in sys.argv]):
new_args = sys.argv[0:2]
i = 2
while i < len(sys.argv):
arg = sys.argv[i]
prm = params[i - 2]

if arg.startswith('--') and not arg.endswith('--'):
new_args += sys.argv[i:i + 2]
i += 2
else:
new_args.append(f'--{prm.name}')
new_args.append(f'{arg}')
i += 1
sys.argv = new_args

for m_name, method in methods.items():
params = [
inspect.Parameter(name=field, kind=inspect._VAR_KEYWORD)
for field in nt._fields
]
sig = inspect.signature(method)

method.__signature__ = sig.replace(parameters=[
inspect.Parameter(name='self', kind=inspect._VAR_KEYWORD),
*params
Expand Down
Binary file added clima/fire/.DS_Store
Binary file not shown.
23 changes: 23 additions & 0 deletions clima/fire/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright (C) 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""The Python Fire module."""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from clima.fire.core import Fire

__all__ = ['Fire']
228 changes: 228 additions & 0 deletions clima/fire/completion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
# Copyright (C) 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Provides tab completion functionality for CLIs built with Fire."""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from collections import defaultdict
from copy import copy
import inspect

from clima.fire import inspectutils
import six


def Script(name, component, default_options=None):
return _Script(name, _Commands(component), default_options)


def _Script(name, commands, default_options=None):
"""Returns a Bash script registering a completion function for the commands.
Args:
name: The first token in the commands, also the name of the command.
commands: A list of all possible commands that tab completion can complete
to. Each command is a list or tuple of the string tokens that make up
that command.
default_options: A dict of options that can be used with any command. Use
this if there are flags that can always be appended to a command.
Returns:
A string which is the Bash script. Source the bash script to enable tab
completion in Bash.
"""
default_options = default_options or set()
options_map = defaultdict(lambda: copy(default_options))
for command in commands:
start = (name + ' ' + ' '.join(command[:-1])).strip()
completion = _FormatForCommand(command[-1])
options_map[start].add(completion)
options_map[start.replace('_', '-')].add(completion)

bash_completion_template = """# bash completion support for {name}
# DO NOT EDIT.
# This script is autogenerated by fire/completion.py.
_complete-{identifier}()
{{
local start cur opts
COMPREPLY=()
start="${{COMP_WORDS[@]:0:COMP_CWORD}}"
cur="${{COMP_WORDS[COMP_CWORD]}}"
opts="{default_options}"
{start_checks}
COMPREPLY=( $(compgen -W "${{opts}}" -- ${{cur}}) )
return 0
}}
complete -F _complete-{identifier} {command}
"""
start_check_template = """
if [[ "$start" == "{start}" ]] ; then
opts="{completions}"
fi"""

start_checks = '\n'.join(
start_check_template.format(
start=start,
completions=' '.join(sorted(options_map[start]))
)
for start in options_map
)

return (
bash_completion_template.format(
name=name,
command=name,
start_checks=start_checks,
default_options=' '.join(default_options),
identifier=name.replace('/', '').replace('.', '').replace(',', '')
)
)


def _IncludeMember(name, verbose):
if verbose:
return True
if isinstance(name, six.string_types):
return name and name[0] != '_'
return True # Default to including the member


def _Members(component, verbose=False):
"""Returns a list of the members of the given component.
If verbose is True, then members starting with _ (normally ignored) are
included.
Args:
component: The component whose members to list.
verbose: Whether to include private members.
Returns:
A list of tuples (member_name, member) of all members of the component.
"""
if isinstance(component, dict):
members = component.items()
else:
members = inspect.getmembers(component)

return [
(member_name, member)
for member_name, member in members
if _IncludeMember(member_name, verbose)
]


def _CompletionsFromArgs(fn_args):
"""Takes a list of fn args and returns a list of the fn's completion strings.
Args:
fn_args: A list of the args accepted by a function.
Returns:
A list of possible completion strings for that function.
"""
completions = []
for arg in fn_args:
arg = arg.replace('_', '-')
completions.append('--{arg}'.format(arg=arg))
return completions


def Completions(component, verbose=False):
"""Gives possible Fire command completions for the component.
A completion is a string that can be appended to a command to continue that
command. These are used for TAB-completions in Bash for Fire CLIs.
Args:
component: The component whose completions to list.
verbose: Whether to include all completions, even private members.
Returns:
A list of completions for a command that would so far return the component.
"""
if inspect.isroutine(component) or inspect.isclass(component):
spec = inspectutils.GetFullArgSpec(component)
return _CompletionsFromArgs(spec.args + spec.kwonlyargs)

if isinstance(component, (tuple, list)):
return [str(index) for index in range(len(component))]

if inspect.isgenerator(component):
# TODO: There are currently no commands available for generators.
return []

return [
_FormatForCommand(member_name)
for member_name, unused_member in _Members(component, verbose)
]


def _FormatForCommand(token):
"""Replaces underscores with hyphens, unless the token starts with a token.
This is because we typically prefer hyphens to underscores at the command
line, but we reserve hyphens at the start of a token for flags. This becomes
relevant when --verbose is activated, so that things like __str__ don't get
transformed into --str--, which would get confused for a flag.
Args:
token: The token to transform.
Returns:
The transformed token.
"""
if not isinstance(token, six.string_types):
token = str(token)

if token.startswith('_'):
return token

return token.replace('_', '-')


def _Commands(component, depth=3):
"""Yields tuples representing commands.
To use the command from Python, insert '.' between each element of the tuple.
To use the command from the command line, insert ' ' between each element of
the tuple.
Args:
component: The component considered to be the root of the yielded commands.
depth: The maximum depth with which to traverse the member DAG for commands.
Yields:
Tuples, each tuple representing one possible command for this CLI.
Only traverses the member DAG up to a depth of depth.
"""
if inspect.isroutine(component) or inspect.isclass(component):
for completion in Completions(component):
yield (completion,)
if inspect.isroutine(component):
return # Don't descend into routines.

if depth < 1:
return

for member_name, member in _Members(component):
# TODO: Also skip components we've already seen.
member_name = _FormatForCommand(member_name)

yield (member_name,)

for command in _Commands(member, depth - 1):
yield (member_name,) + command
Loading

0 comments on commit c4c43bb

Please sign in to comment.