diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index ee5abec..0000000 --- a/.coveragerc +++ /dev/null @@ -1,9 +0,0 @@ -[run] -branch = True -include = argopt/* -omit = - argopt/tests/* - argopt/_docopt.py -relative_files = True -[report] -show_missing = True diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e95ba47..f83ab91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,31 +14,29 @@ jobs: with: fetch-depth: 0 - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - name: set PYSHA + - name: Prepare cache run: echo "PYSHA=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV - uses: actions/cache@v1 with: path: ~/.cache/pre-commit key: pre-commit|${{ env.PYSHA }}|${{ hashFiles('.pre-commit-config.yaml') }} - - name: Test + - name: Dependencies run: | - pip install -U tox - tox -e setup.py - - name: Self install - run: pip install -U .[dev] - - name: Build - run: | - python setup.py sdist bdist_wheel - twine check dist/* + pip install -U pre-commit - uses: reviewdog/action-setup@v1 - if: github.event_name != 'schedule' - name: flake8 + name: Comment run: | - pre-commit run -a flake8 | reviewdog -f=pep8 -name=Format -tee -reporter=github-check -filter-mode nofilter + if [[ $EVENT == pull_request ]]; then + REPORTER=github-pr-review + else + REPORTER=github-check + fi + pre-commit run -a todo | reviewdog -efm="%f:%l: %m" -name=TODO -tee -reporter=$REPORTER -filter-mode nofilter + pre-commit run -a flake8 | reviewdog -f=pep8 -name=flake8 -tee -reporter=$REPORTER -filter-mode nofilter env: REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + EVENT: ${{ github.event_name }} - name: Lint run: pre-commit run -a --show-diff-on-failure test: @@ -66,11 +64,11 @@ jobs: fi env: PYVER: ${{ matrix.python }} - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - name: Coveralls Parallel - uses: AndreMiras/coveralls-python-action@develop - with: - parallel: true + COVERALLS_FLAG_NAME: py${{ matrix.python }} + COVERALLS_PARALLEL: true + COVERALLS_SERVICE_NAME: github + # coveralls needs explicit token + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} finish: if: github.event_name != 'pull_request' || github.head_ref != 'devel' name: Coverage @@ -78,10 +76,13 @@ jobs: needs: test runs-on: ubuntu-latest steps: + - uses: actions/setup-python@v2 - name: Coveralls Finished - uses: AndreMiras/coveralls-python-action@develop - with: - parallel-finished: true + run: | + pip install -U coveralls + coveralls --finish || : + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} deploy: if: github.event_name != 'pull_request' || github.head_ref != 'devel' name: Deploy @@ -91,20 +92,16 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 - - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') - uses: casperdcl/deploy-pypi@v1 + - uses: actions/setup-python@v2 + - id: dist + uses: casperdcl/deploy-pypi@v2 with: build: true - gpg_key: ${{ secrets.GPG_KEY }} password: ${{ secrets.PYPI_TOKEN }} - skip_existing: true - - id: collect_assets - name: Collect assets + gpg_key: ${{ secrets.GPG_KEY }} + upload: ${{ github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') }} + - name: Changelog run: | - echo "::set-output name=asset_path::$(ls dist/*.whl)" - echo "::set-output name=asset_name::$(basename dist/*.whl)" - echo "::set-output name=asset_path_sig::$(ls dist/*.whl.asc 2>/dev/null)" - echo "::set-output name=asset_name_sig::$(basename dist/*.whl.asc 2>/dev/null)" git log --pretty='format:%d%n- %s%n%b---' $(git tag --sort=v:refname | tail -n2 | head -n1)..HEAD > _CHANGES.md - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') id: create_release @@ -122,8 +119,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ${{ steps.collect_assets.outputs.asset_path }} - asset_name: ${{ steps.collect_assets.outputs.asset_name }} + asset_path: dist/${{ steps.dist.outputs.whl }} + asset_name: ${{ steps.dist.outputs.whl }} asset_content_type: application/zip - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') uses: actions/upload-release-asset@v1 @@ -131,6 +128,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ${{ steps.collect_assets.outputs.asset_path_sig }} - asset_name: ${{ steps.collect_assets.outputs.asset_name_sig }} + asset_path: dist/${{ steps.dist.outputs.whl_asc }} + asset_name: ${{ steps.dist.outputs.whl_asc }} asset_content_type: text/plain diff --git a/.gitignore b/.gitignore index e08feb6..c80a818 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,14 @@ *.py[co] +__pycache__/ + # build /argopt/_dist_ver.py /.eggs/ -/*.egg-info +/*.egg*/ /build/ /dist/ + # test -.tox/ -.coverage -__pycache__/ +/.tox/ +/.coverage* +/coverage.xml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c6c7843..78f4b2d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,27 +2,49 @@ default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.3.0 + rev: v3.4.0 hooks: - id: check-added-large-files - id: check-case-conflict - id: check-docstring-first - id: check-executables-have-shebangs - id: check-toml + - id: check-merge-conflict - id: check-yaml + - id: debug-statements - id: end-of-file-fixer - id: mixed-line-ending + - id: sort-simple-yaml - id: trailing-whitespace -- hooks: +- repo: local + hooks: + - id: todo + name: Check TODO + language: pygrep + entry: WIP + args: [-i] + types: [text] + exclude: ^(.pre-commit-config.yaml|.github/workflows/test.yml)$ + - id: nose + name: Run tests + language: python + entry: nosetests + args: ['-d', tests/] + types: [python] + pass_filenames: false + additional_dependencies: + - nose +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.4 + hooks: - id: flake8 + args: ['-j8'] additional_dependencies: - flake8-bugbear - flake8-comprehensions - flake8-debugger - flake8-string-format - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 -- hooks: +- repo: https://github.com/PyCQA/isort + rev: 5.7.0 + hooks: - id: isort - repo: https://github.com/timothycrosley/isort - rev: 5.6.4 diff --git a/Makefile b/Makefile index d4e71da..e31b38b 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ test: tox --skip-missing-interpreters -p all testnose: - nosetests argopt -d -v + nosetests -d -v tests/ testsetup: python setup.py check --metadata --restructuredtext --strict @@ -61,10 +61,10 @@ testsetup: testcoverage: @make coverclean - nosetests argopt --with-coverage --cover-package=argopt --cover-erase --cover-min-percentage=80 -d -v + nosetests --with-coverage --cover-package=argopt --cover-erase --cover-min-percentage=80 -d -v tests/ testtimer: - nosetests argopt --with-timer -d -v + nosetests --with-timer -d -v tests/ distclean: @+make coverclean @@ -78,13 +78,14 @@ prebuildclean: @+python -c "import os; os.remove('argopt/_dist_ver.py') if os.path.exists('argopt/_dist_ver.py') else None" coverclean: @+python -c "import os; os.remove('.coverage') if os.path.exists('.coverage') else None" + @+python -c "import os, glob; [os.remove(i) for i in glob.glob('.coverage.*')]" @+python -c "import shutil; shutil.rmtree('argopt/__pycache__', True)" - @+python -c "import shutil; shutil.rmtree('argopt/tests/__pycache__', True)" + @+python -c "import shutil; shutil.rmtree('tests/__pycache__', True)" clean: @+python -c "import os, glob; [os.remove(i) for i in glob.glob('*.py[co]')]" @+python -c "import os, glob; [os.remove(i) for i in glob.glob('argopt/*.py[co]')]" @+python -c "import os, glob; [os.remove(i) for i in glob.glob('examples/*.py[co]')]" - @+python -c "import os, glob; [os.remove(i) for i in glob.glob('argopt/tests/*.py[co]')]" + @+python -c "import os, glob; [os.remove(i) for i in glob.glob('tests/*.py[co]')]" toxclean: @+python -c "import shutil; shutil.rmtree('.tox', True)" diff --git a/README.rst b/README.rst index 8075774..586675b 100644 --- a/README.rst +++ b/README.rst @@ -95,7 +95,8 @@ checking and default positional arguments. __version__ = "0.1.2-3.4" - parser = argopt(__doc__, version=__version__).parse_args() + parser = argopt(__doc__, version=__version__) + args = parser.parse_args() if args.force: print(args) else: diff --git a/argopt/_argopt.py b/argopt/_argopt.py index e2f3248..5c4b101 100644 --- a/argopt/_argopt.py +++ b/argopt/_argopt.py @@ -106,7 +106,6 @@ def docopt_parser(doc='', logLevel=logging.NOTSET, **_kwargs): set_nargs(args, once_args, None) # setting to `1` creates single-item list set_nargs(args, qest_args, '?') - set_nargs(args, qest_args, '?') set_nargs(args, star_args, '*') set_nargs(args, plus_args, '+') diff --git a/argopt/_docopt.py b/argopt/_docopt.py index e95bbbc..56fd6ae 100644 --- a/argopt/_docopt.py +++ b/argopt/_docopt.py @@ -111,14 +111,21 @@ def __init__(self, name, value=None, desc=None, typ=None): self.value = value self.desc = desc self.type = typ + self._fix_value_type() + + def _fix_value_type(self): + value, typ = self.value, self.type if (value is not None) and (typ is None): if type(value) is bool: self.type = bool - elif type(value) is str: - i = value.rfind(':') - if i >= 0: - self.type = eval(value[i + 1:]) - self.value = typecast(value[:i], value[i + 1:]) + elif hasattr(value, 'rsplit'): + i = value.rsplit(':', 1) + if len(i) == 2: + # potentially used in `eval`, e.g. `partial(open, mode="w")` + from functools import partial # NOQA: F401 + + self.type = eval(i[1]) + self.value = typecast(i[0], i[1]) def __repr__(self): return '%s(%r, %r, %r)' % (self.__class__.__name__, self.name, @@ -208,15 +215,7 @@ def __init__(self, short=None, long=None, argcount=0, value=False, self.type = typ self.desc = desc self.meta = meta - - if (value is not None) and (typ is None): - if type(value) is bool: - self.type = bool - else: - i = value.rfind(':') - if i >= 0: - self.type = eval(value[i + 1:]) - self.value = typecast(value[:i], value[i + 1:]) + self._fix_value_type() @classmethod def parse(class_, option_description): diff --git a/argopt/_utils.py b/argopt/_utils.py index dbdad72..828c37b 100644 --- a/argopt/_utils.py +++ b/argopt/_utils.py @@ -1,13 +1,19 @@ +import logging import subprocess +# potentially used in `eval`, e.g. `partial(open, mode="w")` +from functools import partial # NOQA: F401 + __all__ = ["_range", "typecast", "set_nargs", "_sh", "DictAttrWrap"] -try: # pragma: no cover +try: # py2 _range = xrange -except NameError: # pragma: no cover +except NameError: # py3 _range = range file = open +log = logging.getLogger(__name__) + class DictAttrWrap(object): """Converting docopt-style dictionaries to argparse-style""" @@ -26,7 +32,11 @@ def typecast(val, typ): return None if not isinstance(typ, str): typ = str(typ).lstrip("") - return eval(typ + '(' + str(val) + ')') + try: + return eval(typ + '(' + str(val) + ')') + except Exception: + log.error("Could not evaluate `%s(%s)`. Maybe missing quotes?", typ, val) + raise def set_nargs(all_args, args, n): @@ -34,11 +44,12 @@ def set_nargs(all_args, args, n): a.nargs = n try: _a = [i for i in all_args if i.name == a.name][0] + except IndexError: + pass + else: a.value = _a.value a.desc = _a.desc a.type = _a.type - except IndexError: - pass def _sh(*cmd, **kwargs): diff --git a/setup.cfg b/setup.cfg index e9eaa91..d4ef13e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,21 +1,21 @@ [metadata] -name = argopt -url = https://github.com/casperdcl/argopt -project_urls = - Changelog = https://github.com/casperdcl/argopt/releases - Documentation = https://github.com/casperdcl/argopt/#argopt -licence = MPL 2.0 -license_file = LICENCE -description = doc to argparse driven by docopt -long_description = file: README.rst -long_description_content_type = text/x-rst -author = Casper da Costa-Luis -author_email = casper.dcl@physics.org -keywords = docopt, argparse, doc, docstring, commandline, argument, option, optional, parameter, positional, console, terminal, command line, CLI, UI, gui, gooey -platforms = any -provides = argopt +name=argopt +url=https://github.com/casperdcl/argopt +project_urls= + Changelog=https://github.com/casperdcl/argopt/releases + Documentation=https://github.com/casperdcl/argopt/#argopt +license=MPL 2.0 +license_file=LICENCE +description=doc to argparse driven by docopt +long_description=file: README.rst +long_description_content_type=text/x-rst +author=Casper da Costa-Luis +author_email=casper.dcl@physics.org +keywords=docopt, argparse, doc, docstring, commandline, argument, option, optional, parameter, positional, console, terminal, command line, CLI, UI, gui, gooey +platforms=any +provides=argopt # Trove classifiers (https://pypi.org/pypi?%3Aaction=list_classifiers) -classifiers = +classifiers= Development Status :: 5 - Production/Stable Environment :: Console Environment :: MacOS X @@ -45,7 +45,6 @@ classifiers = Programming Language :: Python :: 2.6 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.2 Programming Language :: Python :: 3.3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 @@ -74,23 +73,34 @@ classifiers = Topic :: Terminals Topic :: Utilities [options] -setup_requires = setuptools>=42; setuptools_scm[toml]>=3.4 -install_requires = +setup_requires=setuptools>=42; setuptools_scm[toml]>=3.4 +install_requires= argparse; python_version == "2.6" -python_requires = >=2.6, !=3.0.*, !=3.1.* -tests_require = nose; flake8; coverage -packages = find: +python_requires=>=2.6, !=3.0.*, !=3.1.* +tests_require=nose; flake8; coverage +packages=find: [options.extras_require] -dev = py-make>=0.1.0; twine; wheel; pre-commit +dev=py-make>=0.1.0; twine; wheel +[options.packages.find] +exclude=tests [bdist_wheel] -universal = 1 +universal=1 [flake8] # TODO: fix & remove C405,C407 after py26 deprecation extend-ignore=E203,P1,C405,C407 -max_line_length = 88 -exclude = .eggs,.tox,dist,build,dist,.git,__pycache__ +max_line_length=88 +exclude=.eggs,.tox,build,dist,.git,__pycache__ [isort] -profile = black -known_first_party = argopt,tests +profile=black +known_first_party=argopt,tests + +[coverage:run] +branch=True +include=argopt/* +omit= + argopt/_docopt.py +relative_files=True +[coverage:report] +show_missing=True diff --git a/argopt/tests/tests_argopt.py b/tests/tests_argopt.py similarity index 100% rename from argopt/tests/tests_argopt.py rename to tests/tests_argopt.py diff --git a/argopt/tests/tests_utils.py b/tests/tests_utils.py similarity index 100% rename from argopt/tests/tests_utils.py rename to tests/tests_utils.py diff --git a/tox.ini b/tox.ini index 75f3686..a7c2e1b 100644 --- a/tox.ini +++ b/tox.ini @@ -4,61 +4,46 @@ # and then run "tox" from this directory. [tox] -# deprecation warning: py{26,32,33,34} -envlist = py{26,27,33,34,35,36,37,38,39,py,py3}, flake8, setup.py -isolated_build = True +# deprecation warning: py{26,27,py2,33,34,35} +envlist=py{26,27,33,34,35,36,37,38,39,py2,py3}, setup.py +isolated_build=True [core] -deps = +deps= nose argparse -commands = nosetests -d -v argopt/ + coverage + codecov -[coverage] -deps = +[testenv] +passenv=TOXENV CI GITHUB_* CODECOV_* COVERALLS_* +deps= {[core]deps} - coverage coveralls -commands = - nosetests --with-coverage --cover-package=argopt -d -v argopt/ - - coveralls - -[extra] -deps = - {[coverage]deps} nose-timer - codecov -commands = - nosetests --with-coverage --with-timer --cover-package=argopt -d -v argopt/ +commands= + nosetests --with-coverage --with-timer --cover-package=argopt -d -v tests/ + coverage xml - coveralls - codecov - -[testenv] -passenv = CI TOXENV CODECOV_* COVERALLS_* -deps = {[extra]deps} -commands = {[extra]commands} + codecov -X pycov -e TOXENV [testenv:py26] -deps = +deps= {[core]deps} - coverage coveralls==1.2.0 - codecov pycparser==2.18 idna==2.7 -commands = - {[coverage]commands} - codecov - -[testenv:flake8] -deps = flake8 -commands = flake8 -j 8 --count --statistics . +commands= + nosetests --with-coverage --cover-package=argopt -d -v tests/ + coverage xml + - coveralls + codecov -X pycov -e TOXENV [testenv:setup.py] -deps = +deps= docutils pygments py-make>=0.1.0 -commands = +commands= {envpython} setup.py check --restructuredtext --metadata --strict {envpython} setup.py make none