From 3403025e03dd626d270f8f075fb4862daeff9878 Mon Sep 17 00:00:00 2001 From: Anthon van der Neut Date: Thu, 27 Aug 2015 17:44:39 +0200 Subject: - added license - move test to _test - move _action to action - updated setup.py (and pkg_data in __init__.py) - flask8 in tox.ini --- .hgignore | 1 + LICENSE | 21 +++ MANIFEST.in | 1 - __init__.py | 39 +++-- _action/__init__.py | 2 - _action/checksinglestore.py | 19 --- _action/count.py | 22 --- _action/splitappend.py | 29 ---- _test/test_argparse.py | 106 +++++++++++++ _test/test_program.py | 232 ++++++++++++++++++++++++++++ action/__init__.py | 2 + action/checksinglestore.py | 19 +++ action/count.py | 23 +++ action/splitappend.py | 29 ++++ example/checksingleaction.py | 2 +- example/testcmd.py | 5 +- setup.py | 349 +++++++++++++++++++++++++++++++++++-------- test/test_argparse.py | 109 -------------- test/test_program.py | 235 ----------------------------- tox.ini | 15 +- 20 files changed, 756 insertions(+), 504 deletions(-) create mode 100644 LICENSE delete mode 100644 MANIFEST.in delete mode 100644 _action/__init__.py delete mode 100644 _action/checksinglestore.py delete mode 100644 _action/count.py delete mode 100644 _action/splitappend.py create mode 100644 _test/test_argparse.py create mode 100644 _test/test_program.py create mode 100644 action/__init__.py create mode 100644 action/checksinglestore.py create mode 100644 action/count.py create mode 100644 action/splitappend.py delete mode 100644 test/test_argparse.py delete mode 100644 test/test_program.py diff --git a/.hgignore b/.hgignore index 6007b8b..dd78dbd 100644 --- a/.hgignore +++ b/.hgignore @@ -11,3 +11,4 @@ dist build *.egg-info .tox +ruamel diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f6f753a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ + + The MIT License (MIT) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index f1cb737..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include setup.py diff --git a/__init__.py b/__init__.py index f9058ce..76928cd 100644 --- a/__init__.py +++ b/__init__.py @@ -1,16 +1,19 @@ # coding: utf-8 -# Copyright Ruamel bvba 2007-2014 from __future__ import print_function -# from six -import sys -PY3 = sys.version_info[0] == 3 - -if PY3: - string_types = str, -else: - string_types = basestring, +# install_requires of ruamel.base is not really required but the old +# ruamel.base installed __init__.py, and thus a new version should +# be installed at some point +_package_data = dict( + full_package_name="ruamel.std.argparse", + version_info=(0, 6, 0), + author='Anthon van der Neut', + author_email='a.van.der.neut@ruamel.eu', + description="Enhancements to argparse: extra actions, subparser aliases, smart formatter, a decorator based wrapper", # NOQA + entry_points=None, + install_requires=['ruamel.base>=1.0.0'], +) # < from ruamel.util.new import _convert_version @@ -33,13 +36,21 @@ def _convert_version(tup): # < -version_info = (0, 5, 2) +version_info = _package_data['version_info'] __version__ = _convert_version(version_info) del _convert_version +import sys import argparse -from argparse import ArgumentParser +from argparse import ArgumentParser # NOQA + +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str, +else: + string_types = basestring, class SubParsersAction(argparse._SubParsersAction): @@ -77,9 +88,9 @@ class SubParsersAction(argparse._SubParsersAction): return parser -from ._action.checksinglestore import CheckSingleStoreAction -from ._action.count import CountAction -from ._action.splitappend import SplitAppendAction +from .action.checksinglestore import CheckSingleStoreAction # NOQA +from .action.count import CountAction # NOQA +from .action.splitappend import SplitAppendAction # NOQA class SmartFormatter(argparse.HelpFormatter): diff --git a/_action/__init__.py b/_action/__init__.py deleted file mode 100644 index f49013d..0000000 --- a/_action/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright Ruamel bvba 2007-2014 -"""extra actions for argparse""" diff --git a/_action/checksinglestore.py b/_action/checksinglestore.py deleted file mode 100644 index 3a37792..0000000 --- a/_action/checksinglestore.py +++ /dev/null @@ -1,19 +0,0 @@ -# coding: utf-8 -# Copyright Ruamel bvba 2007-2014 - -from __future__ import print_function - -import argparse - - -class CheckSingleStoreAction(argparse.Action): - """issue a warning when the store action is called multiple times""" - def __call__(self, parser, namespace, values, option_string=None): - if getattr(namespace, self.dest, None) is not None: - print( - 'WARNING: previous optional argument "' + option_string + " " + - str(getattr(namespace, self.dest)) + '" overwritten by "' + - str(option_string) + - " " + str(values) + - '"') - setattr(namespace, self.dest, values) diff --git a/_action/count.py b/_action/count.py deleted file mode 100644 index 77bf54f..0000000 --- a/_action/count.py +++ /dev/null @@ -1,22 +0,0 @@ -# coding: utf-8 -# Copyright Ruamel bvba 2007-2014 - -from __future__ import print_function - -import argparse - - -class CountAction(argparse.Action): - """argparse action for counting up and down - - parser = argparse.ArgumentParser() - parser.add_argument('--verbose', '-v', action=CountAction, const=1, nargs=0) - parser.add_argument('--quiet', '-q', action=CountAction, dest='verbose', - const=-1, nargs=0) - """ - def __call__(self, parser, namespace, values, option_string=None): - try: - val = getattr(namespace, self.dest) + self.const - except TypeError: # probably None - val = self.const - setattr(namespace, self.dest, val) diff --git a/_action/splitappend.py b/_action/splitappend.py deleted file mode 100644 index 82b003b..0000000 --- a/_action/splitappend.py +++ /dev/null @@ -1,29 +0,0 @@ -# coding: utf-8 -# Copyright Ruamel bvba 2007-2014 - -from __future__ import print_function - -import argparse - - -class SplitAppendAction(argparse._AppendAction): - """append to list, like normal "append", but split first - (default split on ',') - - parser = argparse.ArgumentParser() - parser.add_argument('-d', action=SplitAppendAction) - - the following argument have the same list as result: - -d ab -d cd -d kl -d mn - -d ab,cd,kl,mn - -d ab,cd -d kl,mn - """ - def __init__(self, *args, **kw): - self._split_chr = ',' - argparse.Action.__init__(self, *args, **kw) - - def __call__(self, parser, namespace, values, option_string=None): - # _AppendAction does not return a value - for value in values.split(self._split_chr): - argparse._AppendAction.__call__( - self, parser, namespace, value, option_string) diff --git a/_test/test_argparse.py b/_test/test_argparse.py new file mode 100644 index 0000000..72b8d7a --- /dev/null +++ b/_test/test_argparse.py @@ -0,0 +1,106 @@ +# coding: utf-8 +# Copyright Ruamel bvba 2007-2014 + +import pytest # NOQA < so flake doesn't complain about not using + +from ruamel.std.argparse import argparse, SmartFormatter + +from textwrap import dedent + + +def exit(self=None, status=None, message=None): + pass + + +def test_argparse(capsys): + desc = dedent("""\ + Please do not mess up this text! + -------------------------------- + I have indented it + exactly the way + I want it + """) + help_verbose = "add some verbosity to the output" + help_list = """\ + choose one: + 1) red + 2) green + 3) blue + """ + help_one = """one + line + help + """ + parser = argparse.ArgumentParser( + description=desc, + formatter_class=SmartFormatter, + ) + parser.exit = exit + parser.add_argument('--verbose', action='store_true', + help=help_verbose) + parser.add_argument('--list', help='R|' + dedent(help_list)) + parser.add_argument('--oneline', action='store_true', help=help_one) + parser.parse_args(['--help']) + out, err = capsys.readouterr() + full_help = dedent("""\ + usage: py.test [-h] [--verbose] [--list LIST] [--oneline] + + {0} + optional arguments: + -h, --help show this help message and exit + --verbose {1} + --list LIST {2} + --oneline one line help + """).format( + desc, help_verbose, + help_list.lstrip().replace('\n ', '\n ').rstrip(), + ) + assert full_help == out + + +def test_argparse_default(capsys): + desc = dedent("""\ + Please do not mess up this text! + -------------------------------- + I have indented it + exactly the way + I want it + """) + help_verbose = "add some verbosity to the output" + help_list = """\ + choose one: + 1) red + 2) green + 3) blue + """ + help_one = """one + line + help + """ + parser = argparse.ArgumentParser( + description=desc, + formatter_class=SmartFormatter, + ) + parser.exit = exit + # add "D|" to the first option + parser.add_argument('--verbose', action='store_true', + help='D|' + help_verbose) + parser.add_argument('--list', help='R|' + dedent(help_list)) + parser.add_argument('--oneline', action='store_true', help=help_one) + parser.parse_args(['--help']) + out, err = capsys.readouterr() + full_help = dedent("""\ + usage: py.test [-h] [--verbose] [--list LIST] [--oneline] + + {0} + optional arguments: + -h, --help show this help message and exit + --verbose {1} (default: False) + --list LIST {2} + (default: None) + --oneline one line help (default: False) + """).format( + desc, help_verbose, + help_list.lstrip().replace('\n ', '\n ').rstrip(), + ) + assert full_help == out diff --git a/_test/test_program.py b/_test/test_program.py new file mode 100644 index 0000000..d67b7e0 --- /dev/null +++ b/_test/test_program.py @@ -0,0 +1,232 @@ +# coding: utf-8 +# Copyright Ruamel bvba 2007-2014 + + +from __future__ import print_function + +import sys +import pytest + +from ruamel.std.argparse import ProgramBase, option, sub_parser, version + + +class Program(ProgramBase): + def __init__(self): + # super(Program, self).__init__( + # formatter_class=SmartFormatter + # ) + ProgramBase.__init__(self) + + def run(self): + print('here', self._args.func) + if self._args.func: + return self._args.func() + + # you can put these options on __init__, but if Program is going + # to be subclassed, there will be another __init__ scanned + # in ProgramBase.__init__ than the one decorated here + # defensive is to use a differently named option or the special _pb_init + @option('--verbose', global_option=True, action='store_true') + @option('--quiet', action='store_true') + # @option('--version', action='version', version='version: 42') + @version('version: 42') + def _pb_init(self): + pass + + @sub_parser(help="call mercurial") + @option('--show', action='store_true') + @option('--no-show', help='do not show', metavar='NSHO') + @option('file-name', nargs='*') + def hg(self): + pass + + # have to define hg.sub_parser after creating sub_parser + @hg.sub_parser(help='check something') + @option('--extra') + def check(self): + pass + + @check.sub_parser(help='check something') + def lablab(self): + pass + + @check.sub_parser(help='check something') + def k(self): + print('doing k') + + @check.sub_parser(help='check something') + def m(self): + pass + + @sub_parser(help="call git") + def git(self): + print('doing git') + + @git.sub_parser('abc') + @option('--extra') + def just_some_name(self): + print('doing just_some_name/abc') + + @git.sub_parser('hihi', help='helphelp') + def hki(self): + pass + + @hki.sub_parser('oops') + def oops(self): + print('doing oops') + + @sub_parser(help="call a") + def a(self): + pass + + @sub_parser(help="call b") + def b(self): + pass + + @sub_parser(help="call c") + def c(self): + pass + + @sub_parser(help="call d") + def d(self): + pass + + # on purpose not in "right" order + @sub_parser(help="call f") + def f(self): + print('doing f') + + @sub_parser(help="call e") + def e(self): + pass + + # @sub_parser('svn') + # def subversion(self): + # pass + + +class ParseHelpOutput: + def __init__(self, capsys, error=False): + self._capsys = capsys + out, err = self._capsys.readouterr() + o = err if error else out + self(o) + + def __call__(self, out): + print(out) + print('+++++') + self._chunks = {} + chunk = None + for line in out.splitlines(): + lsplit = line.split() + chunk_name = None + if lsplit and lsplit[0][-1] == ':': + chunk_name = lsplit[0] + line = line.split(':', 1)[1] + if line and line[-1] == ':': + chunk_name = line + if chunk_name: + chunk_name = chunk_name[:-1] + chunk = self._chunks.setdefault(chunk_name, []) + if chunk is None or not line.strip(): + continue + chunk.append(line) + print('chunks', self._chunks) + if not self._chunks: + print('stderr', 'chunks') + + def start(self, chunk, s, strip=True): + """check if a stripped line in the chunk text starts with s""" + for l in self._chunks[chunk]: + if l.lstrip().startswith(s): + return True + return False + + def somewhere(self, chunk, s, strip=True): + """check if s is somewhere in the chunk""" + for l in self._chunks[chunk]: + if s in l: + return True + return False + + def between_curly_braces(self, chunk, elems): + """check if elements are in comma splitted contents of {}""" + for l in self._chunks[chunk]: + start_idx = l.find('{') + end_idx = l.find('}') + test_elems = l[start_idx + 1:end_idx].split(',') + for elem in elems: + if elem not in test_elems: + break + else: + return True + return False + + +@pytest.fixture(scope='class') +def program(): + return Program() + + +class TestProgram: + def test_help(self, capsys, program): + with pytest.raises(SystemExit): + program._parse_args('-h'.split()) + pho = ParseHelpOutput(capsys) + assert pho.start('positional arguments', 'hg') + if sys.version_info[:2] == (2, 6): + # 2.6 argparse scrambles order + x = 'a b c d git'.split() + assert pho.between_curly_braces('usage', x) + else: + assert pho.somewhere('usage', 'c,d,f,e') + assert pho.start('optional arguments', '--verbose') + + def test_help_sub_parser(self, capsys, program): + with pytest.raises(SystemExit): + program._parse_args('hg -h'.split()) + pho = ParseHelpOutput(capsys) + assert pho.start('positional arguments', 'file-name') + assert pho.start('optional arguments', '--verbose') + assert not pho.start('optional arguments', '--extra') + + def test_sub_sub_parser(self, capsys, program): + with pytest.raises(SystemExit): + program._parse_args('hg check -h'.split()) + pho = ParseHelpOutput(capsys) + # assert not pho.start('positional arguments', 'file-name') + # assert not pho.start('positional arguments', 'hg') + assert pho.start('optional arguments', '--extra') + assert pho.start('optional arguments', '--verbose') + + def test_git_help_sub_parser(self, capsys, program): + with pytest.raises(SystemExit): + program._parse_args('git -h'.split()) + pho = ParseHelpOutput(capsys) + assert pho.start('optional arguments', '--verbose') + assert not pho.start('optional arguments', '--extra') + + def test_git_sub_sub_parser(self, capsys, program): + with pytest.raises(SystemExit): + program._parse_args('git abc -h'.split()) + pho = ParseHelpOutput(capsys) + assert pho.start('optional arguments', '--extra') + assert pho.start('optional arguments', '--verbose') + + def test_git_sub_sub_sub_parser(self, capsys, program): + with pytest.raises(SystemExit): + program._parse_args('git hihi oops -h'.split()) + pho = ParseHelpOutput(capsys) + assert pho.start('usage', 'py.test git hihi oops') + assert pho.start('optional arguments', '--verbose') + + def test_version(self, capsys, program): + with pytest.raises(SystemExit): + program._parse_args('--version'.split()) + pho = ParseHelpOutput(capsys, error=sys.version_info < (3, 4)) + assert pho.start('version', '42') + +if __name__ == '__main__': + p = Program() + p._parse_args() + p.run() diff --git a/action/__init__.py b/action/__init__.py new file mode 100644 index 0000000..f49013d --- /dev/null +++ b/action/__init__.py @@ -0,0 +1,2 @@ +# Copyright Ruamel bvba 2007-2014 +"""extra actions for argparse""" diff --git a/action/checksinglestore.py b/action/checksinglestore.py new file mode 100644 index 0000000..3a37792 --- /dev/null +++ b/action/checksinglestore.py @@ -0,0 +1,19 @@ +# coding: utf-8 +# Copyright Ruamel bvba 2007-2014 + +from __future__ import print_function + +import argparse + + +class CheckSingleStoreAction(argparse.Action): + """issue a warning when the store action is called multiple times""" + def __call__(self, parser, namespace, values, option_string=None): + if getattr(namespace, self.dest, None) is not None: + print( + 'WARNING: previous optional argument "' + option_string + " " + + str(getattr(namespace, self.dest)) + '" overwritten by "' + + str(option_string) + + " " + str(values) + + '"') + setattr(namespace, self.dest, values) diff --git a/action/count.py b/action/count.py new file mode 100644 index 0000000..a3d0a36 --- /dev/null +++ b/action/count.py @@ -0,0 +1,23 @@ +# coding: utf-8 +# Copyright Ruamel bvba 2007-2014 + +from __future__ import print_function + +import argparse + + +class CountAction(argparse.Action): + """argparse action for counting up and down + + parser = argparse.ArgumentParser() + parser.add_argument('--verbose', '-v', action=CountAction, const=1, + nargs=0) + parser.add_argument('--quiet', '-q', action=CountAction, dest='verbose', + const=-1, nargs=0) + """ + def __call__(self, parser, namespace, values, option_string=None): + try: + val = getattr(namespace, self.dest) + self.const + except TypeError: # probably None + val = self.const + setattr(namespace, self.dest, val) diff --git a/action/splitappend.py b/action/splitappend.py new file mode 100644 index 0000000..82b003b --- /dev/null +++ b/action/splitappend.py @@ -0,0 +1,29 @@ +# coding: utf-8 +# Copyright Ruamel bvba 2007-2014 + +from __future__ import print_function + +import argparse + + +class SplitAppendAction(argparse._AppendAction): + """append to list, like normal "append", but split first + (default split on ',') + + parser = argparse.ArgumentParser() + parser.add_argument('-d', action=SplitAppendAction) + + the following argument have the same list as result: + -d ab -d cd -d kl -d mn + -d ab,cd,kl,mn + -d ab,cd -d kl,mn + """ + def __init__(self, *args, **kw): + self._split_chr = ',' + argparse.Action.__init__(self, *args, **kw) + + def __call__(self, parser, namespace, values, option_string=None): + # _AppendAction does not return a value + for value in values.split(self._split_chr): + argparse._AppendAction.__call__( + self, parser, namespace, value, option_string) diff --git a/example/checksingleaction.py b/example/checksingleaction.py index 054891c..f4d8166 100644 --- a/example/checksingleaction.py +++ b/example/checksingleaction.py @@ -7,4 +7,4 @@ parser = argparse.ArgumentParser() parser.add_argument('--check', '-c', action=CheckSingleStoreAction, const=1, nargs=0) -print(parser.parse_args("--check -c".split())) \ No newline at end of file +print(parser.parse_args("--check -c".split())) diff --git a/example/testcmd.py b/example/testcmd.py index 1618820..d5a3079 100644 --- a/example/testcmd.py +++ b/example/testcmd.py @@ -1,7 +1,6 @@ -from __future__ import print_function +# coding: utf-8 -import sys -import os +from __future__ import print_function from ruamel.std.argparse import ProgramBase, option, sub_parser, version, \ SmartFormatter diff --git a/setup.py b/setup.py index 3f45fa1..81b3fb8 100644 --- a/setup.py +++ b/setup.py @@ -1,41 +1,68 @@ -#! /usr/bin/env python +# # header # coding: utf-8 -# Copyright Ruamel bvba 2007-2014 from __future__ import print_function -import sys -import os -from textwrap import dedent +if __name__ != '__main__': + raise NotImplementedError('should never include setup.py') + +# # definitions -name_space = 'ruamel' -package_name = 'argparse' -full_package_name = name_space + '.std.' + package_name +full_package_name = None + + +def _package_data(fn): + data = {} + with open(fn) as fp: + parsing = False + for line in fp.readlines(): + if line.startswith('_package_data'): + parsing = True + continue + if not parsing: + continue + if line.startswith(')'): + break + if '# NOQA' in line: + line = line.split('# NOQA', 1)[0].rstrip() + k, v = [x.strip() for x in line.split('=', 1)] + if v[-1] == ',': + v = v[:-1] + if v[0] in '\'"' and v[0] == v[-1]: + data[k] = v[1:-1] + elif v == 'None': + data[k] = None + elif v == 'True': + data[k] = True + elif v == 'False': + data[k] = False + elif v[0] == '(' and v[-1] == ')': + data[k] = tuple([x.strip()[1:-1] if x[0] in '\'"' else int(x) + for x in v[1:-1].split(', ')]) + elif v[0] == '[' and v[-1] == ']': + data[k] = [x.strip()[1:-1] if x[0] in '\'"' else int(x) + for x in v[1:-1].split(', ')] + else: + print('Unknown: >>>>> {0!r} {1!r}'.format(k, v)) + return data + +pkg_data = _package_data('__init__.py') exclude_files = [ 'setup.py', ] -if __name__ == '__main__': - # put here so setup.py can be imported more easily - from setuptools import setup, find_packages, Extension - from setuptools.command import install_lib - +# # imports +import os +import sys -# < from ruamel.util.new.setupinc import get_version, _check_convert_version -def get_version(): - v_i = 'version_info = ' - for line in open('__init__.py'): - if not line.startswith(v_i): - continue - s_e = line[len(v_i):].strip()[1:-1].split(', ') - elems = [x.strip()[1:-1] if x[0] in '\'"' else int(x) for x in s_e] - break - return elems +from setuptools import setup +from setuptools.command import install_lib +# # helper def _check_convert_version(tup): - """create a PEP 386 pseudo-format conformant string from tuple tup""" + """Create a PEP 386 pseudo-format conformant string from tuple tup.""" ret_val = str(tup[0]) # first is always digit next_sep = "." # separator for next extension, can be "" or "." nr_digits = 0 # nr of adjacent digits in rest, to verify @@ -44,7 +71,7 @@ def _check_convert_version(tup): if isinstance(x, int): nr_digits += 1 if nr_digits > 2: - raise ValueError("to many consecutive digits " + ret_val) + raise ValueError("too many consecutive digits " + ret_val) ret_val += next_sep + str(x) next_sep = '.' continue @@ -65,19 +92,13 @@ def _check_convert_version(tup): return ret_val -# < from ruamel.util.new.setupinc import version_info, version_str -version_info = get_version() +version_info = pkg_data['version_info'] version_str = _check_convert_version(version_info) -# < from ruamel.util.new.setupinc import MyInstallLib class MyInstallLib(install_lib.install_lib): - "create __init__.py on the fly" - def run(self): - install_lib.install_lib.run(self) - def install(self): - fpp = full_package_name.split('.') # full package path + fpp = pkg_data['full_package_name'].split('.') # full package path full_exclude_files = [os.path.join(*(fpp + [x])) for x in exclude_files] alt_files = [] @@ -92,44 +113,240 @@ class MyInstallLib(install_lib.install_lib): return alt_files -# < +class NameSpacePackager(object): + def __init__(self, pkg_data): + assert isinstance(pkg_data, dict) + self._pkg_data = pkg_data + self.full_package_name = self._pkg_data['full_package_name'] + self._split = None + self.depth = self.full_package_name.count('.') + self.command = None + if sys.argv[0] == 'setup.py' and sys.argv[1] == 'install' and \ + '--single-version-externally-managed' not in sys.argv: + print('error: have to install with "pip install ."') + sys.exit(1) + for x in sys.argv: + if x[0] == '-' or x == 'setup.py': + continue + self.command = x + break + @property + def split(self): + """split the full package name in list of compontents""" + if self._split is None: + fpn = self.full_package_name.split('.') + self._split = [] + while fpn: + self._split.insert(0, '.'.join(fpn)) + fpn = fpn[:-1] + for d in os.listdir('.'): + if not os.path.isdir(d) or d == self._split[0] or d[0] == '_': + continue + x = os.path.join(d, '__init__.py') + if os.path.exists(x): + self._split.append(self.full_package_name + '.' + d) + return self._split -def main(): - install_requires = [ - "ruamel.base==0.3", - ] - if sys.version_info[:2] == (2, 6): - install_requires.append("argparse") - packages = [full_package_name] + [(full_package_name + '.' + x) for x - in find_packages(exclude=['test'])] - setup( - name=full_package_name, - version=version_str, - description="Enhancements to argparse: extra actions, subparser " - "aliases, smart formatter, a decorator based wrapper", - install_requires=install_requires, - long_description=open('README.rst').read(), - url='https://bitbucket.org/ruamel/std.' + package_name, - author='Anthon van der Neut', - author_email='a.van.der.neut@ruamel.eu', - license="MIT license", - package_dir={full_package_name: '.'}, - namespace_packages=[name_space], - packages=packages, - cmdclass={'install_lib': MyInstallLib}, - classifiers=[ - 'Development Status :: 4 - Beta', + @property + def namespace_packages(self): + return self.split[:self.depth] + + @property + def package_dir(self): + return { + # don't specify empty dir, clashes with package_data spec + self.full_package_name: '.', + self.split[0]: self.split[0], + } + + def create_dirs(self): + """create the directories necessary for namespace packaging""" + if not os.path.exists(self.split[0]): + for d in self.split[:self.depth]: + d = os.path.join(*d.split('.')) + os.mkdir(d) + with open(os.path.join(d, '__init__.py'), 'w') as fp: + fp.write('import pkg_resources\n' + 'pkg_resources.declare_namespace(__name__)\n') + os.symlink( + # a.b gives a/b -> .. + # a.b.c gives a/b/c -> ../.. + os.path.join(*['..'] * self.depth), + os.path.join(*self.split[self.depth].split('.')) + ) + + def check(self): + try: + from pip.exceptions import InstallationError + except ImportError: + return + # arg is either develop (pip install -e) or install + if self.command not in ['install', 'develop']: + return + + # if hgi and hgi.base are both in namespace_packages matching + # against the top (hgi.) it suffices to find minus-e and non-minus-e + # installed packages. As we don't know the order in namespace_packages + # do some magic + prefix = self.split[0] + prefixes = set([prefix, prefix.replace('_', '-')]) + for p in sys.path: + if not p: + continue # directory with setup.py + if os.path.exists(os.path.join(p, 'setup.py')): + continue # some linked in stuff might not be hgi based + if not os.path.isdir(p): + continue + if p.startswith('/tmp/'): + continue + for fn in os.listdir(p): + for pre in prefixes: + if fn.startswith(pre): + break + else: + continue + full_name = os.path.join(p, fn) + # not in prefixes the toplevel is never changed from _ to - + if fn == prefix and os.path.isdir(full_name): + # directory -> other, non-minus-e, install + if self.command == 'develop': + raise InstallationError( + 'Cannot mix develop (pip install -e),\nwith ' + 'non-develop installs for package name {0}'.format( + fn)) + elif fn == prefix: + raise InstallationError( + 'non directory package {0} in {1}'.format( + fn, p)) + for pre in [x + '.' for x in prefixes]: + if fn.startswith(pre): + break + else: + continue # hgiabc instead of hgi. + if fn.endswith('-link') and self.command == 'install': + raise InstallationError( + 'Cannot mix non-develop with develop\n(pip install -e)' + ' installs for package name {0}'.format(fn)) + + def entry_points(self, script_name=None, package_name=None): + ep = self._pkg_data.get('entry_points', True) + if ep is None: + return None + if ep is not True: + return {'console_scripts': [ep]} + if package_name is None: + package_name = self.full_package_name + if not script_name: + script_name = package_name.split('.')[-1] + return {'console_scripts': [ + '{0} = {1}:main'.format(script_name, package_name), + ]} + + @property + def url(self): + return 'https://bitbucket.org/{0}/{1}'.format( + *self.full_package_name.split('.', 1)) + + @property + def author(self): + return self._pkg_data['author'] + + @property + def author_email(self): + return self._pkg_data['author_email'] + + @property + def license(self): + # lic = self._pkg_data.get('license') + # if lic is None: + # lic_file_name = os.path.join(os.path.dirname(__file__), 'LICENSE') + # assert os.path.exists(lic_file_name) + return "MIT license" + + @property + def description(self): + return self._pkg_data['description'] + + @property + def status(self): + # αβ + status = self._pkg_data.get('status', 'β') + if status == 'α': + return (3, 'Alpha') + elif status == 'β': + return (4, 'Beta') + elif 'stable' in status.lower(): + return (5, 'Production/Stable') + raise NotImplementedError + + @property + def classifiers(self): + return [ + 'Development Status :: {0} - {1}'.format(*self.status), 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', + 'License :: ' + ('Other/Proprietary License' + if self._pkg_data.get('license') else + 'OSI Approved :: MIT License'), 'Operating System :: OS Independent', 'Programming Language :: Python', ] + + @property + def install_requires(self): + return pkg_data.get('install_requires', []) + + @property + def data_files(self): + df = self._pkg_data.get('data_files', []) + if self._pkg_data.get('license') is None: + df.append('LICENSE') + if not df: + return None + return [('.', df), ] + + @property + def package_data(self): + df = self._pkg_data.get('data_files', []) + if self._pkg_data.get('license') is None: + # include the file + df.append('LICENSE') + # but don't install it + exclude_files.append('LICENSE') + if not df: + return None + return {self.full_package_name: df} + + +# # call setup +def main(): + nsp = NameSpacePackager(pkg_data) + nsp.check() + nsp.create_dirs() + kw = dict( + name=nsp.full_package_name, + namespace_packages=nsp.namespace_packages, + version=version_str, + packages=nsp.split, + url=nsp.url, + author=nsp.author, + author_email=nsp.author_email, + cmdclass={'install_lib': MyInstallLib}, + package_dir=nsp.package_dir, + entry_points=nsp.entry_points(), + description=nsp.description, + install_requires=nsp.install_requires, + license=nsp.license, + classifiers=nsp.classifiers, + package_data=nsp.package_data, ) + if '--version' not in sys.argv: + for k in sorted(kw): + v = kw[k] + print(k, '->', v) + with open('README.rst') as fp: + kw['long_description'] = fp.read() + setup(**kw) + -if __name__ == '__main__': - if len(sys.argv) > 1 and sys.argv[1] == 'sdist': - assert full_package_name == os.path.abspath(os.path.dirname( - __file__)).split('site-packages' + os.path.sep)[1].replace( - os.path.sep, '.') - main() +main() diff --git a/test/test_argparse.py b/test/test_argparse.py deleted file mode 100644 index 9cc818b..0000000 --- a/test/test_argparse.py +++ /dev/null @@ -1,109 +0,0 @@ -# coding: utf-8 -# Copyright Ruamel bvba 2007-2014 - -import pytest - -try: - from ruamel.std.argparse import argparse, CountAction, SmartFormatter -except ImportError: - print("you have to install ruamel.std.argparse to run the tests") - -from textwrap import dedent - - -def exit(self=None, status=None, message=None): - pass - - -def test_argparse(capsys): - desc = dedent("""\ - Please do not mess up this text! - -------------------------------- - I have indented it - exactly the way - I want it - """) - help_verbose = "add some verbosity to the output" - help_list = """\ - choose one: - 1) red - 2) green - 3) blue - """ - help_one = """one - line - help - """ - parser = argparse.ArgumentParser( - description=desc, - formatter_class=SmartFormatter, - ) - parser.exit = exit - parser.add_argument('--verbose', action='store_true', - help=help_verbose) - parser.add_argument('--list', help='R|' + dedent(help_list)) - parser.add_argument('--oneline', action='store_true', help=help_one) - parser.parse_args(['--help']) - out, err = capsys.readouterr() - full_help = dedent("""\ - usage: py.test [-h] [--verbose] [--list LIST] [--oneline] - - {0} - optional arguments: - -h, --help show this help message and exit - --verbose {1} - --list LIST {2} - --oneline one line help - """).format( - desc, help_verbose, - help_list.lstrip().replace('\n ', '\n ').rstrip(), - ) - assert full_help == out - - -def test_argparse_default(capsys): - desc = dedent("""\ - Please do not mess up this text! - -------------------------------- - I have indented it - exactly the way - I want it - """) - help_verbose = "add some verbosity to the output" - help_list = """\ - choose one: - 1) red - 2) green - 3) blue - """ - help_one = """one - line - help - """ - parser = argparse.ArgumentParser( - description=desc, - formatter_class=SmartFormatter, - ) - parser.exit = exit - # add "D|" to the first option - parser.add_argument('--verbose', action='store_true', - help='D|' + help_verbose) - parser.add_argument('--list', help='R|' + dedent(help_list)) - parser.add_argument('--oneline', action='store_true', help=help_one) - parser.parse_args(['--help']) - out, err = capsys.readouterr() - full_help = dedent("""\ - usage: py.test [-h] [--verbose] [--list LIST] [--oneline] - - {0} - optional arguments: - -h, --help show this help message and exit - --verbose {1} (default: False) - --list LIST {2} - (default: None) - --oneline one line help (default: False) - """).format( - desc, help_verbose, - help_list.lstrip().replace('\n ', '\n ').rstrip(), - ) - assert full_help == out diff --git a/test/test_program.py b/test/test_program.py deleted file mode 100644 index 4487b65..0000000 --- a/test/test_program.py +++ /dev/null @@ -1,235 +0,0 @@ -# coding: utf-8 -# Copyright Ruamel bvba 2007-2014 - - -from __future__ import print_function - -import sys -import pytest - -try: - from ruamel.std.argparse import ProgramBase, option, sub_parser, version -except ImportError: - print("you have to install ruamel.std.argparse to run the tests") - - -class Program(ProgramBase): - def __init__(self): - # super(Program, self).__init__( - # formatter_class=SmartFormatter - # ) - ProgramBase.__init__(self) - - def run(self): - print('here', self._args.func) - if self._args.func: - return self._args.func() - - # you can put these options on __init__, but if Program is going - # to be subclassed, there will be another __init__ scanned - # in ProgramBase.__init__ than the one decorated here - # defensive is to use a differently named option or the special _pb_init - @option('--verbose', global_option=True, action='store_true') - @option('--quiet', action='store_true') - # @option('--version', action='version', version='version: 42') - @version('version: 42') - def _pb_init(self): - pass - - @sub_parser(help="call mercurial") - @option('--show', action='store_true') - @option('--no-show', help='do not show', metavar='NSHO') - @option('file-name', nargs='*') - def hg(self): - pass - - # have to define hg.sub_parser after creating sub_parser - @hg.sub_parser(help='check something') - @option('--extra') - def check(self): - pass - - @check.sub_parser(help='check something') - def lablab(self): - pass - - @check.sub_parser(help='check something') - def k(self): - print('doing k') - - @check.sub_parser(help='check something') - def m(self): - pass - - @sub_parser(help="call git") - def git(self): - print('doing git') - - @git.sub_parser('abc') - @option('--extra') - def just_some_name(self): - print('doing just_some_name/abc') - - @git.sub_parser('hihi', help='helphelp') - def hki(self): - pass - - @hki.sub_parser('oops') - def oops(self): - print('doing oops') - - @sub_parser(help="call a") - def a(self): - pass - - @sub_parser(help="call b") - def b(self): - pass - - @sub_parser(help="call c") - def c(self): - pass - - @sub_parser(help="call d") - def d(self): - pass - - # on purpose not in "right" order - @sub_parser(help="call f") - def f(self): - print('doing f') - - @sub_parser(help="call e") - def e(self): - pass - - # @sub_parser('svn') - # def subversion(self): - # pass - - -class ParseHelpOutput: - def __init__(self, capsys, error=False): - self._capsys = capsys - out, err = self._capsys.readouterr() - o = err if error else out - self(o) - - def __call__(self, out): - print(out) - print('+++++') - self._chunks = {} - chunk = None - for line in out.splitlines(): - lsplit = line.split() - chunk_name = None - if lsplit and lsplit[0][-1] == ':': - chunk_name = lsplit[0] - line = line.split(':', 1)[1] - if line and line[-1] == ':': - chunk_name = line - if chunk_name: - chunk_name = chunk_name[:-1] - chunk = self._chunks.setdefault(chunk_name, []) - if chunk is None or not line.strip(): - continue - chunk.append(line) - print('chunks', self._chunks) - if not self._chunks: - print('stderr', err) - - def start(self, chunk, s, strip=True): - """check if a stripped line in the chunk text starts with s""" - for l in self._chunks[chunk]: - if l.lstrip().startswith(s): - return True - return False - - def somewhere(self, chunk, s, strip=True): - """check if s is somewhere in the chunk""" - for l in self._chunks[chunk]: - if s in l: - return True - return False - - def between_curly_braces(self, chunk, elems): - """check if elements are in comma splitted contents of {}""" - for l in self._chunks[chunk]: - start_idx = l.find('{') - end_idx = l.find('}') - test_elems = l[start_idx + 1:end_idx].split(',') - for elem in elems: - if elem not in test_elems: - break - else: - return True - return False - - -@pytest.fixture(scope='class') -def program(): - return Program() - - -class TestProgram: - def test_help(self, capsys, program): - with pytest.raises(SystemExit): - program._parse_args('-h'.split()) - pho = ParseHelpOutput(capsys) - assert pho.start('positional arguments', 'hg') - if sys.version_info[:2] == (2, 6): - # 2.6 argparse scrambles order - x = 'a b c d git'.split() - assert pho.between_curly_braces('usage', x) - else: - assert pho.somewhere('usage', 'c,d,f,e') - assert pho.start('optional arguments', '--verbose') - - def test_help_sub_parser(self, capsys, program): - with pytest.raises(SystemExit): - program._parse_args('hg -h'.split()) - pho = ParseHelpOutput(capsys) - assert pho.start('positional arguments', 'file-name') - assert pho.start('optional arguments', '--verbose') - assert not pho.start('optional arguments', '--extra') - - def test_sub_sub_parser(self, capsys, program): - with pytest.raises(SystemExit): - program._parse_args('hg check -h'.split()) - pho = ParseHelpOutput(capsys) - # assert not pho.start('positional arguments', 'file-name') - # assert not pho.start('positional arguments', 'hg') - assert pho.start('optional arguments', '--extra') - assert pho.start('optional arguments', '--verbose') - - def test_git_help_sub_parser(self, capsys, program): - with pytest.raises(SystemExit): - program._parse_args('git -h'.split()) - pho = ParseHelpOutput(capsys) - assert pho.start('optional arguments', '--verbose') - assert not pho.start('optional arguments', '--extra') - - def test_git_sub_sub_parser(self, capsys, program): - with pytest.raises(SystemExit): - program._parse_args('git abc -h'.split()) - pho = ParseHelpOutput(capsys) - assert pho.start('optional arguments', '--extra') - assert pho.start('optional arguments', '--verbose') - - def test_git_sub_sub_sub_parser(self, capsys, program): - with pytest.raises(SystemExit): - program._parse_args('git hihi oops -h'.split()) - pho = ParseHelpOutput(capsys) - assert pho.start('usage', 'py.test git hihi oops') - assert pho.start('optional arguments', '--verbose') - - def test_version(self, capsys, program): - with pytest.raises(SystemExit): - program._parse_args('--version'.split()) - pho = ParseHelpOutput(capsys, error=sys.version_info < (3, 4)) - assert pho.start('version', '42') - -if __name__ == '__main__': - p = Program() - p._parse_args() - p.run() diff --git a/tox.ini b/tox.ini index e3f59c1..f0d6a5d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,17 @@ [tox] -envlist = py26,py27,py33,py34 +envlist = pep8,py27,py34,py26,py33 [testenv] -commands = py.test test +commands = + py.test _test deps = pytest - ruamel.base + flake8 + +[testenv:pep8] +commands = + flake8 {posargs} + +[flake8] +show-source = True +exclude = .hg,.git,.tox,dist,.cache,__pycache__,ruamel.zip2tar.egg-info -- cgit v1.2.1