From 6cbdad50bd1d4b91d67853642eb4f961217e467c Mon Sep 17 00:00:00 2001 From: cloether <20920516+cloether@users.noreply.github.com> Date: Thu, 22 May 2025 00:48:58 -0400 Subject: [PATCH 1/6] updates --- .github/workflows/cleanup.yml | 2 +- .github/workflows/python-package.yml | 2 +- Dockerfile | 4 +- docs/source/conf.py | 72 ++++----- pyproject.toml | 96 +++++++++++ pytest.ini | 69 ++++++-- scripts/apidoc.py | 62 ++++---- scripts/change.py | 79 ++++----- scripts/checkenc.py | 54 +++---- scripts/checklic.py | 12 +- scripts/cleanup.py | 26 +-- scripts/rstlint.py | 230 +++++++++++++-------------- scripts/runtests.py | 20 +-- scripts/winbuild.py | 14 +- skeleton/__init__.py | 31 ++-- skeleton/__main__.py | 6 +- skeleton/__version__.py | 16 +- skeleton/cli.py | 62 ++++---- skeleton/config.py | 87 +++++----- skeleton/const.py | 78 ++++----- skeleton/error.py | 30 ++-- skeleton/log.py | 137 +++++++++++----- skeleton/utils.py | 128 +++++++-------- tests/.coveragerc | 25 ++- tests/conftest.py | 32 ++-- tests/test_cli.py | 14 +- tests/test_config.py | 107 ++++++------- tests/test_error.py | 7 +- tests/test_log.py | 56 ++++--- tests/test_utils.py | 20 +-- tests/utils.py | 26 +-- 31 files changed, 918 insertions(+), 686 deletions(-) create mode 100644 pyproject.toml diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index 0082201..f501b7a 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -6,7 +6,7 @@ jobs: validate-data: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Cleanup temporary files/directories run: | ./scripts/cleanup.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 8d0f1f3..79194b9 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -21,7 +21,7 @@ jobs: matrix: python-version: [ 3.7, 3.8, 3.9 ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: diff --git a/Dockerfile b/Dockerfile index a4238c3..9ec33e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-alpine +FROM python:3.13-alpine RUN mkdir /app WORKDIR /app COPY requirements.txt . @@ -7,4 +7,4 @@ COPY . . ENV PYTHONPATH="/app" RUN addgroup -S projects && adduser -S -H projects -G projects RUN chown -R projects:projects /app -USER projects +USER projects \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index b8b03dd..16efd1e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -4,7 +4,7 @@ This file only contains a selection of the most common options. For a full list see the documentation: - http://www.sphinx-doc.org/en/master/config + https://www.sphinx-doc.org/en/master/config """ from __future__ import absolute_import, print_function, unicode_literals @@ -47,11 +47,11 @@ # https://www.sphinx-doc.org/en/master/usage/extensions/viewcode.html # https://www.sphinx-doc.org/en/master/usage/extensions/todo.html extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.viewcode", - "sphinx.ext.todo", - "sphinxcontrib.napoleon", - "guzzle_sphinx_theme" + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.todo", + "sphinxcontrib.napoleon", + "guzzle_sphinx_theme" ] intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} @@ -93,12 +93,12 @@ # Custom sidebar templates, filenames relative to this file. html_sidebars = { - "**": [ - "logo-text.html", - "globaltoc.html", - "localtoc.html", - "searchbox.html" - ] + "**": [ + "logo-text.html", + "globaltoc.html", + "localtoc.html", + "searchbox.html" + ] } # Output file base name for HTML help builder. @@ -117,25 +117,25 @@ # -- Options for LaTeX output --------------------------------------- latex_elements = { - # Paper size ("letterpaper" or "a4paper"). - # "papersize": "letterpaper", - # - # The font size ("10pt", "11pt" or "12pt"). - # "pointsize": "10pt", - # - # Additional stuff for the LaTeX preamble. - # "preamble": "", + # Paper size ("letterpaper" or "a4paper"). + # "papersize": "letterpaper", + # + # The font size ("10pt", "11pt" or "12pt"). + # "pointsize": "10pt", + # + # Additional stuff for the LaTeX preamble. + # "preamble": "", } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, # documentclass [howto/manual]). latex_documents = [ - ("index", - "{0}.tex".format(project), - "{0} Documentation".format(project), - author, - "manual") + ("index", + "{0}.tex".format(project), + "{0} Documentation".format(project), + author, + "manual") ] # The name of an image file (relative to this directory) to place at @@ -163,10 +163,10 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ("index", project, - "{0} Documentation".format(project), - [author], - 3) + ("index", project, + "{0} Documentation".format(project), + [author], + 3) ] # If true, show URL addresses after external links. @@ -179,13 +179,13 @@ # dir menu entry, description, category) texinfo_documents = [ - ("index", - project, - "{0} Documentation".format(project), - author, - project, - __description__, - "Miscellaneous"), + ("index", + project, + "{0} Documentation".format(project), + author, + project, + __description__, + "Miscellaneous"), ] # Documents to append as an appendix to all manuals. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a6f9b76 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,96 @@ +# pyproject.toml is a configuration file that is used to +# specify the build system requirements for a Python project. +# +# References: +# - [PEP-518](https://peps.python.org/pep-0518/) +# - [PEP-517](https://peps.python.org/pep-0517/) +# - [PEP-621](https://peps.python.org/pep-0621/) +# - [PEP-660](https://peps.python.org/pep-0660/) +# - [Python Packaging Reference - pyproject.toml](https://packaging.python.org/en/latest/specifications/pyproject-toml/#pyproject-toml-specification +# +# JSON Schema: +# { +# "$schema": "http://json-schema.org/schema#", +# "type": "object", +# "additionalProperties": false, +# "properties": { +# "build-system": { +# "type": "object", +# "additionalProperties": false, +# "properties": { +# "requires": { +# "type": "array", +# "items": { +# "type": "string" +# } +# } +# }, +# "required": [ +# "requires" +# ] +# }, +# "tool": { +# "type": "object" +# } +# } +# } + +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "skeleton" +version = "0.0.10" +description = "Python Module Template" +readme = "README.md" +license = { text = "MIT" } +authors = [{ name = "Chad Loether", email = "chad.loether@outlook.com" }] +keywords = ["template", "skeleton"] +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Natural Language :: English", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Topic :: Software Development :: Libraries :: Python Modules" +] +dependencies = ["six", "appdirs"] + +[project.urls] +Source = "https://github.com/cloether/skeleton" +Tracker = "https://github.com/cloether/skeleton/issues" + +[tool.setuptools.packages.find] +include = ["skeleton"] +exclude = ["doc*", "example*", "script*", "test*"] + +[project.optional-dependencies] +docs = ["sphinx", "sphinxcontrib-napoleon", "guzzle_sphinx_theme"] +tests = [ + "check-manifest", + "coverage", + "pycodestyle", + "pytest", + "pytest-cov", + "pytest-html", + "tox", + "tox-travis", + "twine", + "wheel" +] +python2.6 = ["ordereddict==1.1", "simplejson==3.3.0"] +python2.7 = ["ipaddress"] + +[project.scripts] +skeleton = "skeleton.__main__:main" \ No newline at end of file diff --git a/pytest.ini b/pytest.ini index 04a9519..f1c370e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,21 +1,72 @@ -; References: +; pytest.ini +; +; references: ; https://docs.pytest.org/en/stable/ ; https://plugincompat.herokuapp.com/ -;suppress inspection for section "LongLine" + [pytest] -addopts = -rN --tb=short -p no:warnings --cache-clear --cov . --html=tests/reports/pytest.html --cov-config 'tests/.coveragerc' --self-contained-html -cache_dir = tests/.pytest_cache -log_cli = 1 + +# minimum compatible pytest version +minversion = 7.0 + +; required plugins and version constraints +required_plugins = + pytest-cov>=6.1.1 + pytest-html>=4.1.1 + pytest-json-report>=1.5.0 + +# cli arguments to pass to pytest +# TODO: look into '--cov=.' +addopts = + -rN + --tb=short + -p no:warnings + --cache-clear + --cov=. + --cov-config=tests/.coveragerc + --cov-report=html:tests/reports/coverage.html + --cov-report=xml:tests/reports/coverage.xml + --cov-report=json:tests/reports/coverage.json + --cov-report=lcov:tests/reports/coverage.lcov + --self-contained-html + --html=tests/reports/results.html + --junitxml=tests/reports/results.xml + --json-report + --json-report-file=tests/reports/results.json + --json-report-indent=2 + +; discover test files in this directory +testpaths = tests +python_files = tests/test_*.py +norecursedirs = + .git + .idea + .tox + scripts + build + examples + docs + +; logging configuration for CLI +log_cli = true log_cli_date_format = %Y-%m-%d %H:%M:%S log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) log_cli_level = DEBUG + +; logging configuration for file output log_file = tests/logs/pytest.log log_file_date_format = %Y-%m-%d %H:%M:%S log_file_format = %(asctime)s [%(name)s] %(levelname)s: %(message)s log_file_level = DEBUG log_level = DEBUG -norecursedirs = .git .idea .tox scripts build examples docs -python_files = tests/test_*.py + +; pytest cache location +cache_dir = tests/.pytest_cache + +; additional pytest-html setting (optional) render_collapsed = 1 -required_plugins = pytest-html pytest-cov -testpaths = tests + +; marker declarations (if custom marks are used) +markers = + slow: marks tests as slow (use with '-m "not slow"') + integration: marks integration tests \ No newline at end of file diff --git a/scripts/apidoc.py b/scripts/apidoc.py index 2f0facb..3a28cd0 100755 --- a/scripts/apidoc.py +++ b/scripts/apidoc.py @@ -88,8 +88,8 @@ def _run(command, **kwargs): return_code = check_call(command, **kwargs) except CalledProcessError as e: LOGGER.exception( - "error occurred while running command: " - "command=\"{0}\" returncode={1}".format(command, e.returncode) + "error occurred while running command: " + "command=\"{0}\" returncode={1}".format(command, e.returncode) ) return_code = e.returncode return return_code @@ -106,8 +106,8 @@ def run(command, location=None): int: Return Code. """ LOGGER.debug( - 'running command: command="%s" location="%s"', - command, location + 'running command: command="%s" location="%s"', + command, location ) if location is not None: with cwd(location): @@ -137,7 +137,11 @@ def module_name(exclude=("doc*", "example*", "script*", "test*"), where=".", Returns: str: Module name if found otherwise None. """ - packages = find_packages(exclude=exclude, where=where, include=include) + packages = find_packages( + exclude=exclude, + where=where, + include=include + ) return next(iter(packages), default) @@ -174,24 +178,20 @@ def docs_update(parser, args): readme_rst = os.path.join(repo_root, "README.rst") readme_content = _readfile(readme_rst, mode="rb") readme_hash = _hash_md5(readme_content) - LOGGER.debug('readme: filepath="%s" md5="%s"', readme_rst, readme_hash) index_rst = os.path.join(repo_root, "docs", "source", "index.rst") index_content = _readfile(index_rst, mode="rb")[:len(readme_content) + 1] index_hash = _hash_md5(index_content) - LOGGER.debug('index: filepath="%s" md5="%s"', index_rst, index_hash) should_update = not readme_hash == index_hash - LOGGER.debug("needs-update: %s", should_update) if not should_update: return 0 readme_content = _readfile(readme_rst) - readme_content += ( os.linesep + INDEX_TEMPLATE.format(module_name(where=repo_root)) ) @@ -211,48 +211,46 @@ def main(**kwargs): LOGGER.debug("creating argument parser: kwargs=%s", kwargs) parser = ArgumentParser(**kwargs) - parser.set_defaults( - argument_default=SUPPRESS, - conflict_handler="resolve", - description="documentation utilities", - formatter_class=ArgumentDefaultsHelpFormatter, - prog=os.path.splitext(os.path.basename(__file__))[0], + argument_default=SUPPRESS, + conflict_handler="resolve", + description="documentation utilities", + formatter_class=ArgumentDefaultsHelpFormatter, + prog=os.path.splitext(os.path.basename(__file__))[0], ) - parser.add_argument( - "-d", "--debug", - action="store_true", - help="debug logging" + "-d", "--debug", + action="store_true", + help="debug logging" ) sub = parser.add_subparsers( - title="Command", - description="command to run (default: generate)", - dest="command" + title="Command", + description="command to run (default: generate)", + dest="command" ) # build build_parser = sub.add_parser( - "build", - add_help=False, - help="build full documentation from generated markup files" + "build", + add_help=False, + help="build full documentation from generated markup files" ) build_parser.set_defaults(func=docs_build, command="build") # generate generate_parser = sub.add_parser( - "generate", - add_help=False, - help="generate documentation markup files from source" + "generate", + add_help=False, + help="generate documentation markup files from source" ) generate_parser.set_defaults(func=docs_generate, command="generate") # update update_parser = sub.add_parser( - "update", - add_help=False, - help="update index.rst from README.rst" # TODO: support markdown + "update", + add_help=False, + help="update index.rst from README.rst" # TODO: support markdown ) update_parser.set_defaults(func=docs_update, command="update") @@ -261,7 +259,7 @@ def main(**kwargs): args = parser.parse_args() logging.basicConfig( - level=logging.DEBUG if args.debug else logging.CRITICAL + level=logging.DEBUG if args.debug else logging.CRITICAL ) return args.func(parser, args) diff --git a/scripts/change.py b/scripts/change.py index 78e2433..393115c 100755 --- a/scripts/change.py +++ b/scripts/change.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 # coding=utf8 -"""Generate a new changelog entry. +"""change.py + +Generate a new changelog entry. Usage ===== @@ -9,13 +11,14 @@ scripts/new-change -This will open up a file in your editor (via the ``EDITOR`` env var). +This will open up a file in your editor (via the ``EDITOR`` env var). You will see this template:: # Type should be one of: feature, bugfix type: # Category is the high level feature area. - # This can be a service identifier (e.g ``s3``), or something like: Paginator. + # This can be a service identifier (e.g. ``s3``), + # or something like: ``Paginator``. category: # A brief description of the change. You can use GitHub style @@ -24,10 +27,10 @@ description: -Fill in the appropriate values, save and exit the editor. +Fill in the appropriate values, save, and exit the editor. Make sure to commit these changes as part of your pull request. -If, when your editor is open, you decide don't want to add +If, when your editor is open, you decide do not want to add a changelog entry, save an empty file and no entry will be generated. @@ -86,13 +89,13 @@ def new_changelog_entry(args): """ # get values from change content parsed_values = ( - { - 'type': args.change_type, - 'category': args.category, - 'description': args.description - } - if all_values_provided(args) - else get_values_from_editor(args) + { + 'type': args.change_type, + 'category': args.category, + 'description': args.description + } + if all_values_provided(args) + else get_values_from_editor(args) ) # exit if empty values are found @@ -138,9 +141,9 @@ def get_values_from_editor(args, template=CHANGE_TEMPLATE): """ with NamedTemporaryFile('w') as f: contents = template.format( - change_type=args.change_type, - category=args.category, - description=args.description, + change_type=args.change_type, + category=args.category, + description=args.description, ) f.write(contents) f.flush() @@ -153,8 +156,8 @@ def get_values_from_editor(args, template=CHANGE_TEMPLATE): with open(f.name) as _f: filled_in_contents = _f.read() - parsed_values = parse_filled_in_contents(filled_in_contents) + return parsed_values @@ -168,9 +171,9 @@ def url(repository_name, issue): """ issue_number = issue[1:] return '`{0} `__'.format( - issue, - repository_name, - issue_number + issue, + repository_name, + issue_number ) def link(match): @@ -209,17 +212,17 @@ def _valid_char(x): return x in _VALID_CHARS filename = '{type_name}-{summary}'.format( - type_name=parsed_values['type'], - summary=''.join(filter(_valid_char, category)) + type_name=parsed_values['type'], + summary=''.join(filter(_valid_char, category)) ) possible_filename = os.path.join( - dirname, '{0}-{1}.json'.format(filename, random.randint(1, 100000)) + dirname, '{0}-{1}.json'.format(filename, random.randint(1, 100000)) ) while os.path.isfile(possible_filename): possible_filename = os.path.join( - dirname, '{0}-{1}.json'.format(filename, random.randint(1, 100000)) + dirname, '{0}-{1}.json'.format(filename, random.randint(1, 100000)) ) with open(possible_filename, 'w') as f: @@ -244,7 +247,6 @@ def parse_filled_in_contents(contents): return {} parsed = {} - lines = iter(contents.splitlines()) for line in (line.strip() for line in lines): @@ -253,8 +255,10 @@ def parse_filled_in_contents(contents): if 'type' not in parsed and line.startswith('type:'): parsed['type'] = line.split(':')[1].strip() + elif 'category' not in parsed and line.startswith('category:'): parsed['category'] = line.split(':')[1].strip() + elif 'description' not in parsed and line.startswith('description:'): # Assume that everything until the end of the file is part # of the description, so we can break once we pull in the @@ -263,6 +267,7 @@ def parse_filled_in_contents(contents): full_description = '\n'.join([first_line] + list(lines)) parsed['description'] = full_description.strip() break + return parsed @@ -276,25 +281,25 @@ def main(): parser = ArgumentParser() parser.add_argument( - '-t', '--type', - dest='change_type', - default='', - choices=CHANGE_TYPES + '-t', '--type', + dest='change_type', + default='', + choices=CHANGE_TYPES ) parser.add_argument( - '-c', '--category', - dest='category', - default='' + '-c', '--category', + dest='category', + default='' ) parser.add_argument( - '-d', '--description', - dest='description', - default='' + '-d', '--description', + dest='description', + default='' ) parser.add_argument( - '-r', '--repo', - default='owner/repo', - help='Optional repo name, e.g: owner/repo' + '-r', '--repo', + default='owner/repo', + help='Optional repo name, e.g: owner/repo' ) args = parser.parse_args() return new_changelog_entry(args) diff --git a/scripts/checkenc.py b/scripts/checkenc.py index f4dbaa5..8de4cf3 100755 --- a/scripts/checkenc.py +++ b/scripts/checkenc.py @@ -149,7 +149,7 @@ def walk_python_files(paths, is_python=looks_like_python, exclude_dirs=None): for exclude in exclude_dirs: if exclude in dirnames: LOGGER.debug( - "excluded directory: %s", os.path.join(dirpath, exclude) + "excluded directory: %s", os.path.join(dirpath, exclude) ) dirnames.remove(exclude) @@ -271,46 +271,46 @@ def _parse_args(): # noinspection PyTypeChecker parser = ArgumentParser( - prog=os.path.basename(__file__), - description=__doc__, - formatter_class=RawDescriptionHelpFormatter, - epilog="""References: + prog=os.path.basename(__file__), + description=__doc__, + formatter_class=RawDescriptionHelpFormatter, + epilog="""References: https://github.com/python/cpython/blob/master/Tools/scripts/findnocoding.py """ ) parser.add_argument( - "paths", - metavar="PATHS", - nargs="+", - help="search path(s)." + "paths", + metavar="PATHS", + nargs="+", + help="search path(s)." ) parser.add_argument( - "-c", "--compile", - action="store_true", - help="recognize python files by trying to compile. (default: %(default)s)" + "-c", "--compile", + action="store_true", + help="recognize python files by trying to compile. (default: %(default)s)" ) parser.add_argument( - "-e", "--exclude", - nargs="+", - default=[".git", ".idea", "__pycache__"], - help="directories to exclude while searching. (default: %(default)s)" + "-e", "--exclude", + nargs="+", + default=[".git", ".idea", "__pycache__"], + help="directories to exclude while searching. (default: %(default)s)" ) parser.add_argument( - "-d", "--debug", - action="store_true", - help="enable debug logging. (default: %(default)s)" + "-d", "--debug", + action="store_true", + help="enable debug logging. (default: %(default)s)" ) parser.add_argument( - "-v", "--version", - version=__version__, - action="version" + "-v", "--version", + version=__version__, + action="version" ) parser.add_argument( - "-o", "--output", - default="-", - metavar="path", - help="Output Location (default: %(default)s)", - type=FileType("{0!s}+".format("wb" if sys.version_info[0] == 2 else "w")) + "-o", "--output", + default="-", + metavar="path", + help="Output Location (default: %(default)s)", + type=FileType("{0!s}+".format("wb" if sys.version_info[0] == 2 else "w")) ) args = parser.parse_args() if args.debug is True: diff --git a/scripts/checklic.py b/scripts/checklic.py index 129cabe..7e2694b 100755 --- a/scripts/checklic.py +++ b/scripts/checklic.py @@ -33,9 +33,9 @@ """ ENV_DIRS = frozenset([ - os.path.join(ROOT_DIR, 'env'), - os.path.join(ROOT_DIR, 'env27'), - os.path.join(ROOT_DIR, '.tox'), + os.path.join(ROOT_DIR, 'env'), + os.path.join(ROOT_DIR, 'env27'), + os.path.join(ROOT_DIR, '.tox'), ]) @@ -83,9 +83,9 @@ def main(): """Entry Point. """ missing_header = [ - file_path - for file_path, file_contents - in get_files_without_header() + file_path + for file_path, file_contents + in get_files_without_header() ] n = len(missing_header) sys.stderr.write("file(s) missing license header: {0:d}\n".format(n)) diff --git a/scripts/cleanup.py b/scripts/cleanup.py index 383ba5f..c00af63 100755 --- a/scripts/cleanup.py +++ b/scripts/cleanup.py @@ -13,19 +13,19 @@ from six import next TARGETS = [ - # TODO: Add support for globs/regex - (".coverage*",), - (".pytest_cache",), - (".tox",), - ("build",), - ("dist",), - ("docs", "build"), - ("logs",), - ("tests", ".pytest_cache"), - ("tests", "pytest.log"), - ("tests", "logs"), - ("tests", "reports"), - ("tests", "tests") + # TODO: Add support for globs/regex + (".coverage*",), + (".pytest_cache",), + (".tox",), + ("build",), + ("dist",), + ("docs", "build"), + ("logs",), + ("tests", ".pytest_cache"), + ("tests", "pytest.log"), + ("tests", "logs"), + ("tests", "reports"), + ("tests", "tests") ] diff --git a/scripts/rstlint.py b/scripts/rstlint.py index 09adeba..194f754 100755 --- a/scripts/rstlint.py +++ b/scripts/rstlint.py @@ -39,120 +39,120 @@ DEFAULT_ROLE_RE = re.compile(r"(?:^| )`\w(?P[^`]*?\w)?`(?:$| )") DIRECTIVES = [ - # standard docutils ones - "admonition", - "attention", - "caution", - "class", - "compound", - "container", - "contents", - "csv-table", - "danger", - "date", - "default-role", - "epigraph", - "error", - "figure", - "footer", - "header", - "highlights", - "hint", - "image", - "important", - "include", - "line-block", - "list-table", - "meta", - "note", - "parsed-literal", - "pull-quote", - "raw", - "replace", - "restructuredtext-test-directive", - "role", - "rubric", - "sectnum", - "sidebar", - "table", - "target-notes", - "tip", - "title", - "topic", - "unicode", - "warning", - # Sphinx and Python docs custom ones - "acks", - "attribute", - "autoattribute", - "autoclass", - "autodata", - "autoexception", - "autofunction", - "automethod", - "automodule", - "centered", - "cfunction", - "class", - "classmethod", - "cmacro", - "cmdoption", - "cmember", - "code-block", - "confval", - "cssclass", - "ctype", - "currentmodule", - "cvar", - "data", - "decorator", - "decoratormethod", - "deprecated-removed", - "deprecated(?!-removed)", - "describe", - "directive", - "doctest", - "envvar", - "event", - "exception", - "function", - "glossary", - "highlight", - "highlightlang", - "impl-detail", - "index", - "literalinclude", - "method", - "miscnews", - "module", - "moduleauthor", - "opcode", - "pdbcommand", - "productionlist", - "program", - "role", - "sectionauthor", - "seealso", - "sourcecode", - "staticmethod", - "tabularcolumns", - "testcode", - "testoutput", - "testsetup", - "toctree", - "todo", - "todolist", - "versionadded", - "versionchanged", + # standard docutils ones + "admonition", + "attention", + "caution", + "class", + "compound", + "container", + "contents", + "csv-table", + "danger", + "date", + "default-role", + "epigraph", + "error", + "figure", + "footer", + "header", + "highlights", + "hint", + "image", + "important", + "include", + "line-block", + "list-table", + "meta", + "note", + "parsed-literal", + "pull-quote", + "raw", + "replace", + "restructuredtext-test-directive", + "role", + "rubric", + "sectnum", + "sidebar", + "table", + "target-notes", + "tip", + "title", + "topic", + "unicode", + "warning", + # Sphinx and Python docs custom ones + "acks", + "attribute", + "autoattribute", + "autoclass", + "autodata", + "autoexception", + "autofunction", + "automethod", + "automodule", + "centered", + "cfunction", + "class", + "classmethod", + "cmacro", + "cmdoption", + "cmember", + "code-block", + "confval", + "cssclass", + "ctype", + "currentmodule", + "cvar", + "data", + "decorator", + "decoratormethod", + "deprecated-removed", + "deprecated(?!-removed)", + "describe", + "directive", + "doctest", + "envvar", + "event", + "exception", + "function", + "glossary", + "highlight", + "highlightlang", + "impl-detail", + "index", + "literalinclude", + "method", + "miscnews", + "module", + "moduleauthor", + "opcode", + "pdbcommand", + "productionlist", + "program", + "role", + "sectionauthor", + "seealso", + "sourcecode", + "staticmethod", + "tabularcolumns", + "testcode", + "testoutput", + "testsetup", + "toctree", + "todo", + "todolist", + "versionadded", + "versionchanged", ] ALL_DIRECTIVES = "({0})".format("|".join(DIRECTIVES)) LEAKED_MARKDOWN_RE = re.compile(r"[a-z]::\s|`|\.\.\s*\w+:") SEEMS_DIRECTIVE_RE = re.compile( - r"(?[^a-z:]|:(?!:))".format( - ALL_DIRECTIVES - ) + r"(?[^a-z:]|:(?!:))".format( + ALL_DIRECTIVES + ) ) @@ -316,14 +316,14 @@ def rstlint(path, false_pos=False, ignore=None, severity=1, verbose=False): if c_sev >= severity: for n, msg in _checker(fn, lines): sys.stdout.write( - "[{0:d}] PROBLEMS: {1}:{2:d}: {3}\n".format(c_sev, fn, n, msg) + "[{0:d}] PROBLEMS: {1}:{2:d}: {3}\n".format(c_sev, fn, n, msg) ) count[c_sev] += 1 if not count: if severity > 1: sys.stdout.write( - "No Problems With Severity >= {0:d} Found.\n".format(severity) + "No Problems With Severity >= {0:d} Found.\n".format(severity) ) else: sys.stdout.write("No Problems Found.\n") @@ -331,7 +331,7 @@ def rstlint(path, false_pos=False, ignore=None, severity=1, verbose=False): for severity in sorted(count): number = count[severity] sys.stdout.write("{0:d} Problem{1} With Severity {2:d} Found.\n".format( - number, "s" if number > 1 else "", severity + number, "s" if number > 1 else "", severity )) return count @@ -346,10 +346,10 @@ def _parse_args(argv): return 2 arg_d = { - "verbose": False, - "severity": 1, - "ignore": [], - "false_pos": False, + "verbose": False, + "severity": 1, + "ignore": [], + "false_pos": False, } for opt, val in opts: if opt == "-v": diff --git a/scripts/runtests.py b/scripts/runtests.py index 7aa6360..6db361a 100755 --- a/scripts/runtests.py +++ b/scripts/runtests.py @@ -110,6 +110,8 @@ def touch(filepath): fh.close() +# TODO: update for changes in pytest and coverage configs + def main(): """CLI Entry Point. @@ -137,15 +139,15 @@ def main(): tests_html_file = os.path.join(reports_dir, tests_html_filename) return_code = run( - "pytest {posargs} " - "--cov={module} " - "--html={tests_html_file} " - "--self-contained-html".format( - module=module, - tests_html_file=tests_html_file, - envname=env_name, - posargs=tests_dir - ) + "pytest {posargs} " + "--cov={module} " + "--html={tests_html_file} " + "--self-contained-html".format( + module=module, + tests_html_file=tests_html_file, + envname=env_name, + posargs=tests_dir + ) ) return return_code diff --git a/scripts/winbuild.py b/scripts/winbuild.py index 132e069..f5019f4 100755 --- a/scripts/winbuild.py +++ b/scripts/winbuild.py @@ -38,8 +38,8 @@ def win_compile(filename, output_filename, arch=DEFAULT_ARCH, reg_path = r"{0}\Setup\VC".format(msvc9compiler.VS_BASE % vc_ver) vcvars = os.path.join( - msvc9compiler.Reg.get_value(reg_path, "productdir"), - "bin", "vcvars{0:d}.bat".format(arch == "x86" and 32 or 64) + msvc9compiler.Reg.get_value(reg_path, "productdir"), + "bin", "vcvars{0:d}.bat".format(arch == "x86" and 32 or 64) ) path = os.path.splitext(output_filename) @@ -47,7 +47,7 @@ def win_compile(filename, output_filename, arch=DEFAULT_ARCH, obj_filename = "{0}.obj".format(path[0]) command = "\"{0}\" {1} & cl {2} /Fe{3} /Fo{4}".format( - vcvars, arch, filename, output_filename, obj_filename + vcvars, arch, filename, output_filename, obj_filename ) p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -85,10 +85,10 @@ def main(): args = parser.parse_args() try: win_compile( - args.filename, - args.output_filename, - arch=args.arch, - vc_ver=args.vc_ver + args.filename, + args.output_filename, + arch=args.arch, + vc_ver=args.vc_ver ) except Exception as e: sys.stderr.write("{0}\n".format(e)) diff --git a/skeleton/__init__.py b/skeleton/__init__.py index 840e072..2981294 100644 --- a/skeleton/__init__.py +++ b/skeleton/__init__.py @@ -12,21 +12,21 @@ from __future__ import absolute_import, print_function, unicode_literals __all__ = ( - "__app_id__", - "__author__", - "__author_email__", - "__copyright__", - "__description__", - "__license__", - "__title__", - "__url__", - "__version__", - "cli", - "config", - "const", - "error", - "log", - "utils" + "__app_id__", + "__author__", + "__author_email__", + "__copyright__", + "__description__", + "__license__", + "__title__", + "__url__", + "__version__", + "cli", + "config", + "const", + "error", + "log", + "utils" ) from .__version__ import ( @@ -47,6 +47,7 @@ except ImportError: from logging import Handler + # Needed for Backwards Compatibility (Python 2.6). # Note: NullHandler was introduced in Python 2.7. class NullHandler(Handler): diff --git a/skeleton/__main__.py b/skeleton/__main__.py index 3d1a0a3..c6725df 100644 --- a/skeleton/__main__.py +++ b/skeleton/__main__.py @@ -4,7 +4,7 @@ Module CLI Entry Point. Usage: - python -m [options...] + python3 -m [options...] """ from __future__ import absolute_import, print_function, unicode_literals @@ -66,8 +66,8 @@ def _shutdown_handler(signum, frame): # pylint: disable=unused-argument """ sys.stderr.write("\b\b") # write 2 backspaces to stderr sys.stderr.write( - "interrupt detected: signal={0:d} " - "frame=\"{1:s}\"\n".format(signum, frame) + "interrupt detected: signal={0:d} " + "frame=\"{1:s}\"\n".format(signum, frame) ) sys.exit(signum) diff --git a/skeleton/__version__.py b/skeleton/__version__.py index 0efea41..d727451 100644 --- a/skeleton/__version__.py +++ b/skeleton/__version__.py @@ -11,14 +11,14 @@ from __future__ import absolute_import, print_function, unicode_literals __all__ = ( - "__author__", - "__author_email__", - "__copyright__", - "__description__", - "__title__", - "__license__", - "__url__", - "__version__", + "__author__", + "__author_email__", + "__copyright__", + "__description__", + "__title__", + "__license__", + "__url__", + "__version__", ) __author__ = "Chad Loether" diff --git a/skeleton/cli.py b/skeleton/cli.py index 478d948..0f910ae 100644 --- a/skeleton/cli.py +++ b/skeleton/cli.py @@ -64,43 +64,43 @@ def argparser(**kwargs): parser = ArgumentParser(**kwargs) parser.set_defaults( - argument_default=SUPPRESS, - conflict_handler="resolve", - description=__description__, - formatter_class=ArgumentDefaultsHelpFormatter, - usage=__doc__, - prog=__title__, + argument_default=SUPPRESS, + conflict_handler="resolve", + description=__description__, + formatter_class=ArgumentDefaultsHelpFormatter, + usage=__doc__, + prog=__title__, ) parser.add_argument( - "-V", "--version", - action="version", - version=__version__ + "-V", "--version", + action="version", + version=__version__ ) parser.add_argument( - "-d", "--debug", - "-v", "--verbose", - action="store_true", - help="enable verbose logging" + "-d", "--debug", + "-v", "--verbose", + action="store_true", + help="enable verbose logging" ) parser.add_argument( - "-i", "--input", - default="-", - metavar="path", - help="input location (default: %(default)s)", - type=FileType("{0!s}+".format(DEFAULT_FILE_READ_MODE)) + "-i", "--input", + default="-", + metavar="path", + help="input location (default: %(default)s)", + type=FileType("{0!s}+".format(DEFAULT_FILE_READ_MODE)) ) parser.add_argument( - "-o", "--output", - default="-", - metavar="path", - help="output location (default: %(default)s)", - type=FileType("{0!s}+".format(DEFAULT_FILE_WRITE_MODE)) + "-o", "--output", + default="-", + metavar="path", + help="output location (default: %(default)s)", + type=FileType("{0!s}+".format(DEFAULT_FILE_WRITE_MODE)) ) parser.add_argument( - "--logfile", - help="log to file. (default: %(default)s)", - metavar="FILE", - default=LOGGING_FILENAME + "--logfile", + help="log to file. (default: %(default)s)", + metavar="FILE", + default=LOGGING_FILENAME ) return parser @@ -123,10 +123,10 @@ def main(*args, **kwargs): # setup logging logging.basicConfig( - filename=args.logfile, - level=logging.DEBUG if args.debug else LOGGING_LEVEL, - format=LOGGING_FORMAT, - datefmt=LOGGING_DATEFMT + filename=args.logfile, + level=logging.DEBUG if args.debug else LOGGING_LEVEL, + format=LOGGING_FORMAT, + datefmt=LOGGING_DATEFMT ) LOGGER.warning("command line interface not yet implemented.") diff --git a/skeleton/config.py b/skeleton/config.py index 48546fa..b860830 100644 --- a/skeleton/config.py +++ b/skeleton/config.py @@ -42,13 +42,15 @@ LOGGER = logging.getLogger(__name__) -def getenv(keys, default=None, drop_null=True): - """Create Configuration from an environment variables. +def getenv(keys, default=None, ignore_unset=True): + """Create configuration from one or more environment + variable key/values pairs. Args: - keys (list or tuple or str): Environment variables to retrieve. + keys (list or tuple or str): Environment variable key(s) to default: Default value to set for key when no value is found. - drop_null (bool): If True null values will be ignored. + ignore_unset (bool): Does not set the key if the environment + variable value is not set or is empty. Returns: dict: Dict of environment variable key/vales. @@ -63,14 +65,9 @@ def getenv(keys, default=None, drop_null=True): for key in keys: value = os.getenv(key, None) - if drop_null and (not value or value is None): + if ignore_unset and (not value or value is None): continue - - config[key] = ( - value - if value is not None - else default - ) + config[key] = (value if value is not None else default) return config @@ -120,9 +117,9 @@ def import_string(import_name, silent=False): except ImportError as e: if not silent: reraise( - tp=ImportStringError, - value=ImportStringError(import_name, e), - tb=sys.exc_info()[2] + tp=ImportStringError, + value=ImportStringError(import_name, e), + tb=sys.exc_info()[2] ) raise @@ -159,24 +156,24 @@ class Configuration(dict): """ # defaults OPTIONS = OrderedDict([ - ('client_cert', None), - ('connect_timeout', DEFAULT_TIMEOUT), - ('log_datefmt', LOGGING_DATEFMT), - ('log_format', LOGGING_FORMAT), - ('log_file', LOGGING_FILENAME), - ('log_filemode', LOGGING_FILEMODE), - ('log_level', LOGGING_LEVEL), - ('log_style', LOGGING_STYLE), - ('max_pool_connections', DEFAULT_MAX_POOL_CONNECTIONS), - ('poolblock', DEFAULT_POOLBLOCK), - ('poolsize', DEFAULT_POOLSIZE), - ('pool_timeout', DEFAULT_POOL_TIMEOUT), - ('proxies', None), - ('proxies_config', None), - ('read_timeout', DEFAULT_TIMEOUT), - ('retries', DEFAULT_RETRIES), - ('user_agent', __app_id__), - ('verify', False), + ('client_cert', None), + ('connect_timeout', DEFAULT_TIMEOUT), + ('log_datefmt', LOGGING_DATEFMT), + ('log_format', LOGGING_FORMAT), + ('log_file', LOGGING_FILENAME), + ('log_filemode', LOGGING_FILEMODE), + ('log_level', LOGGING_LEVEL), + ('log_style', LOGGING_STYLE), + ('max_pool_connections', DEFAULT_MAX_POOL_CONNECTIONS), + ('poolblock', DEFAULT_POOLBLOCK), + ('poolsize', DEFAULT_POOLSIZE), + ('pool_timeout', DEFAULT_POOL_TIMEOUT), + ('proxies', None), + ('proxies_config', None), + ('read_timeout', DEFAULT_TIMEOUT), + ('retries', DEFAULT_RETRIES), + ('user_agent', __app_id__), + ('verify', False), ]) APP_DIRS = AppDirs(__title__, __author__, __version__) @@ -186,9 +183,9 @@ class Configuration(dict): FILEPATH_USER = os.path.join(APP_DIRS.user_config_dir, FILENAME) SEARCH_PATHS = ( - FILEPATH_SITE, - FILEPATH_USER, - FILENAME + FILEPATH_SITE, + FILEPATH_USER, + FILENAME ) # noinspection PyMissingConstructor @@ -218,9 +215,9 @@ def _make_options(self, *args, **kwargs): # number of args should not be longer than allowed options if len(args) > len(keys): raise TypeError( - "takes at most {0} arguments ({1} given)".format( - len(keys), len(args) - ) + "takes at most {0} arguments ({1} given)".format( + len(keys), len(args) + ) ) # iterate through args passed through to the constructor @@ -229,7 +226,7 @@ def _make_options(self, *args, **kwargs): # if a kwarg was specified for the arg, then error out. if keys[i] in config: raise TypeError( - "multiple values for keyword argument: {0}".format(keys[i]) + "multiple values for keyword argument: {0}".format(keys[i]) ) config[keys[i]] = arg return config @@ -312,18 +309,18 @@ def from_env(cls, default=None, drop_null=True): skeleton.config.Configuration: Configuration Instance. """ return cls(**getenv( - cls.keylist(), - default=default, - drop_null=drop_null + cls.keylist(), + default=default, + ignore_unset=drop_null )) def _update_from_env(self, default=None, drop_null=True): """Update Configuration from environment variables. """ dict.update(self, getenv( - self.keylist(), - default=default, - drop_null=drop_null + self.keylist(), + default=default, + ignore_unset=drop_null )) @classmethod diff --git a/skeleton/const.py b/skeleton/const.py index 4dd8b2f..8c62d2c 100644 --- a/skeleton/const.py +++ b/skeleton/const.py @@ -25,7 +25,7 @@ DEFAULT_TIMEOUT = 60 # FILE DEFAULTS -DEFAULT_CHUNK_SIZE = 64 * 2**10 +DEFAULT_CHUNK_SIZE = 64 * 2 ** 10 DEFAULT_FILE_MODE_SUFFIX = "b" if sys.version_info[0] == 2 else "" DEFAULT_FILE_WRITE_MODE = "w{0}".format(DEFAULT_FILE_MODE_SUFFIX) DEFAULT_FILE_READ_MODE = "r{0}".format(DEFAULT_FILE_MODE_SUFFIX) @@ -38,53 +38,53 @@ LOGGING_FILEMODE = "a+" LOGGING_FILENAME = None LOGGING_FORMAT = ( - "(%(asctime)s) [%(levelname)s] " - "%(name)s.%(funcName)s(%(lineno)d): %(message)s" + "(%(asctime)s) [%(levelname)s] " + "%(name)s.%(funcName)s(%(lineno)d): %(message)s" ) LOGGING_LEVEL = logging.ERROR LOGGING_STYLE = "%" LOGGING_LEVELS = { - logging.NOTSET: "sample", - logging.DEBUG: "debug", - logging.INFO: "info", - logging.WARNING: "warning", - logging.ERROR: "error", - logging.FATAL: "fatal", + logging.NOTSET: "sample", + logging.DEBUG: "debug", + logging.INFO: "info", + logging.WARNING: "warning", + logging.ERROR: "error", + logging.FATAL: "fatal", } LOGGING_LEVELS_MAP = { - LOGGING_LEVELS[lvl]: lvl - for lvl in LOGGING_LEVELS + LOGGING_LEVELS[lvl]: lvl + for lvl in LOGGING_LEVELS } LOGGING_DICT = { - # TODO: implement test(s) - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "standard": { - "format": LOGGING_FORMAT, - "style": LOGGING_STYLE, - "datefmt": LOGGING_DATEFMT - } + # TODO: implement test(s) + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "standard": { + "format": LOGGING_FORMAT, + "style": LOGGING_STYLE, + "datefmt": LOGGING_DATEFMT + } + }, + "handlers": { + "console": { + "level": "INFO", + "class": "logging.StreamHandler", + "formatter": "standard", }, - "handlers": { - "console": { - "level": "INFO", - "class": "logging.StreamHandler", - "formatter": "standard", - }, - "file": { - "class": "logging.FileHandler", - "level": "INFO", - "formatter": "standard", - "filename": "{0}.log".format(DEFAULT_LOGGER_NAME), - "mode": LOGGING_FILEMODE, - }, + "file": { + "class": "logging.FileHandler", + "level": "INFO", + "formatter": "standard", + "filename": "{0}.log".format(DEFAULT_LOGGER_NAME), + "mode": LOGGING_FILEMODE, }, - "loggers": { - DEFAULT_LOGGER_NAME: { - "handlers": ["console"], - "level": "INFO", - "propagate": False - } + }, + "loggers": { + DEFAULT_LOGGER_NAME: { + "handlers": ["console"], + "level": "INFO", + "propagate": False } + } } diff --git a/skeleton/error.py b/skeleton/error.py index 0897f80..2e32c0d 100644 --- a/skeleton/error.py +++ b/skeleton/error.py @@ -6,9 +6,9 @@ from __future__ import absolute_import, print_function, unicode_literals __all__ = ( - "BaseError", - "ImportStringError", - "PathNotFound" + "BaseError", + "ImportStringError", + "PathNotFound" ) @@ -91,24 +91,24 @@ class ImportStringError(BaseError): """Import String Exception. """ fmt = ( - "import_string() failed for {import_name}. possible reasons are:\n\n" - "- missing __init__.py in a package;\n" - "- package or module path not included in sys.path;\n" - "- duplicated package or module name taking precedence in " - "sys.path;\n" - "- missing module, class, function or variable;\n\n" - "debugged import:\n\n{exception_name}\n\n" - "original exception:\n\n{exception}" + "import_string() failed for {import_name}. possible reasons are:\n\n" + "- missing __init__.py in a package;\n" + "- package or module path not included in sys.path;\n" + "- duplicated package or module name taking precedence in " + "sys.path;\n" + "- missing module, class, function or variable;\n\n" + "debugged import:\n\n{exception_name}\n\n" + "original exception:\n\n{exception}" ) def __init__(self, import_name, exception): self.import_name = import_name self.exception = exception BaseError.__init__( - self, - import_name=import_name, - exception_name=exception.__class__.__name__, - exception=exception + self, + import_name=import_name, + exception_name=exception.__class__.__name__, + exception=exception ) diff --git a/skeleton/log.py b/skeleton/log.py index ea83825..741f831 100644 --- a/skeleton/log.py +++ b/skeleton/log.py @@ -21,12 +21,12 @@ from .const import LOGGING_LEVEL __all__ = ( - "log_level", - "log_request", - "log_response", - "log_request_response", - "apply_session_hook", - "add_stderr_logger" + "log_level", + "log_request", + "log_response", + "log_request_response", + "apply_session_hook", + "add_stderr_logger" ) LOGGER = logging.getLogger(__name__) @@ -35,13 +35,13 @@ _CONTENT_DISPOSITION_RE = re.compile(_CONTENT_DISPOSITION_PAT, re.I) _URL_PAT = ( - r"http[s]?://" - r"(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+" + r"http[s]?://" + r"(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+" ) _WS_PAT = ( - r"ws[s]?://" - r"(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+" + r"ws[s]?://" + r"(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+" ) HIDING_PATTERNS = [_URL_PAT, _WS_PAT] @@ -51,30 +51,30 @@ # https://github.com/awslabs/aws-lambda-powertools-python/blob/develop/aws_lambda_powertools/logging/formatter.py RESERVED_LOG_ATTRS = ( - "name", - "msg", - "args", - "level", - "levelname", - "levelno", - "pathname", - "filename", - "module", - "exc_info", - "exc_text", - "stack_info", - "lineno", - "funcName", - "created", - "msecs", - "relativeCreated", - "thread", - "threadName", - "processName", - "process", - "asctime", - "location", - "timestamp", + "name", + "msg", + "args", + "level", + "levelname", + "levelno", + "pathname", + "filename", + "module", + "exc_info", + "exc_text", + "stack_info", + "lineno", + "funcName", + "created", + "msecs", + "relativeCreated", + "thread", + "threadName", + "processName", + "process", + "asctime", + "location", + "timestamp", ) @@ -183,6 +183,68 @@ def log_level(level): return check_level(level) +def log_flask_response(response, **kwargs): + """Log Flask Response. + + Args: + response (flask.Response): Flask Response Object. + """ + if not LOGGER.isEnabledFor(logging.DEBUG): + return + log_content = kwargs.setdefault("log_content", False) + try: + LOGGER.debug("RESPONSE:") # start + LOGGER.debug(" - STATUS CODE: %s", response.status_code) + if response.headers: + LOGGER.debug(" - HEADERS:") + for header, value in iteritems(response.headers): + LOGGER.debug(" - %s: %s", header, value) + if response.data is None: + LOGGER.debug(" - BODY:") + LOGGER.debug(" - (NO-BODY)") + else: + if not log_content: + return + LOGGER.debug(" - BODY:") + LOGGER.debug(" - %s", text_type(response.data)) + except Exception as e: # pylint: disable=broad-except + LOGGER.error("FAILED to log response: %r", e) + LOGGER.debug("--") # end + + +def log_flask_request(request, **kwargs): + """Log Flask Request. + + Args: + request (flask.Request): Flask Request Object. + """ + if not LOGGER.isEnabledFor(logging.DEBUG): + return + log_content = kwargs.setdefault("log_content", False) + try: + LOGGER.debug("REQUEST:") # start + LOGGER.debug(" - URL: %s", request.url) + LOGGER.debug(" - PATH: %s", request.path) + LOGGER.debug(" - METHOD: %s", request.method.upper()) + if request.headers: + LOGGER.debug(" - HEADERS:") + for header, value in iteritems(request.headers): + if header.lower() == "authorization": + value = "*" * len(value) + LOGGER.debug(" - %s: %s", header, value) + if request.data is None: + LOGGER.debug(" - BODY:") + LOGGER.debug(" - (NO-BODY)") + else: + if not log_content: + return + LOGGER.debug(" - BODY:") + LOGGER.debug(" - %s", text_type(request.data)) + except Exception as e: # pylint: disable=broad-except + LOGGER.error("FAILED to log request: %r", e) + LOGGER.debug("--") # end + + def log_request(request, **kwargs): """Log HTTP Request @@ -330,9 +392,9 @@ def add_stderr_logger(level=logging.INFO, fmt=None, datefmt=None, style=None): logger = logging.getLogger(__name__) handler = logging.StreamHandler() handler.setFormatter(logging.Formatter( - fmt=fmt, - datefmt=datefmt, - style=style + fmt=fmt, + datefmt=datefmt, + style=style )) logger.addHandler(handler) logger.setLevel(level) @@ -395,6 +457,7 @@ def convert_match_to_sha3(cls, match): str: Hexadecimal string representation of the provided hashed (SHA3) match value. """ + # noinspection PyArgumentEqualDefault value = ensure_binary(match.group(0), "utf8") return sha3_256(value).digest().hex() diff --git a/skeleton/utils.py b/skeleton/utils.py index def87b8..c66b593 100644 --- a/skeleton/utils.py +++ b/skeleton/utils.py @@ -27,34 +27,34 @@ from .error import PathNotFound __all__ = ( - "advance", - "apply_values", - "as_bool", - "as_number", - "chunk", - "chunkify", - "compress_file", - "cwd", - "DateRange", - "get_file_size", - "group_continuous", - "isatty", - "is_file_newer_than_file", - "iterchunk", - "make_executable", - "memoize", - "mkdir_p", - "run_in_separate_process", - "safe_b64decode", - "script_dir", - "strtobool", - "timedelta_isoformat", - "TIMEDELTA_ZERO", - "timestamp_from_datetime", - "to_valid_filename", - "to_valid_module_name", - "touch", - "typename" + "advance", + "apply_values", + "as_bool", + "as_number", + "chunk", + "chunkify", + "compress_file", + "cwd", + "DateRange", + "get_file_size", + "group_continuous", + "isatty", + "is_file_newer_than_file", + "iterchunk", + "make_executable", + "memoize", + "mkdir_p", + "run_in_separate_process", + "safe_b64decode", + "script_dir", + "strtobool", + "timedelta_isoformat", + "TIMEDELTA_ZERO", + "timestamp_from_datetime", + "to_valid_filename", + "to_valid_module_name", + "touch", + "typename" ) @@ -102,9 +102,9 @@ def safe_b64decode(data): overflow = len(data) % 4 if overflow: data += ( - "=" - if isinstance(data, string_types) - else b"=" + "=" + if isinstance(data, string_types) + else b"=" ) * (4 - overflow) return b64decode(data) @@ -293,15 +293,15 @@ def run_in_separate_process(func, *args, **kwargs): with os.fdopen(write_fd, "wb") as fd: try: # dump results. pickle.dump( - (status, result), - fd, - pickle.HIGHEST_PROTOCOL + (status, result), + fd, + pickle.HIGHEST_PROTOCOL ) except pickle.PicklingError as e: pickle.dump( - (2, e), - fd, - pickle.HIGHEST_PROTOCOL + (2, e), + fd, + pickle.HIGHEST_PROTOCOL ) os._exit(0) # noqa @@ -560,10 +560,10 @@ def chunkify(iterable, size): if size <= 0: raise ValueError("non-positive chunk size: {0}".format(size)) return ( - chunk - if hasattr(iterable, '__getitem__') - # generator, set, map, etc... - else iterchunk + chunk + if hasattr(iterable, '__getitem__') + # generator, set, map, etc... + else iterchunk )(iterable, size) @@ -625,16 +625,16 @@ def timedelta_isoformat(td): minutes, seconds = divmod(td.seconds, 60) hours, minutes = divmod(minutes, 60) return ( - 'P{td.days}DT' - '{hours:d}H' - '{minutes:d}M' - '{seconds:d}' - '.{td.microseconds:06d}S'.format( - td=td, - hours=hours, - minutes=minutes, - seconds=seconds - ) + 'P{td.days}DT' + '{hours:d}H' + '{minutes:d}M' + '{seconds:d}' + '.{td.microseconds:06d}S'.format( + td=td, + hours=hours, + minutes=minutes, + seconds=seconds + ) ) @@ -680,10 +680,10 @@ def __init__(self, start=None, stop=None, step=None): def __repr__(self): return "{!s}(start={!r}, stop={!r}, step={!r}".format( - self.__class__.__name__, - self.start, - self.stop, - self.step + self.__class__.__name__, + self.start, + self.stop, + self.step ) def __reversed__(self): @@ -697,18 +697,18 @@ def __len__(self): # it would be nice if float("inf") could be returned raise TypeError("infinite range") calc = ( - self.start - self.stop - if self._has_neg_step - else self.stop - self.start + self.start - self.stop + if self._has_neg_step + else self.stop - self.start ) return int(ceil(abs(calc.total_seconds() / self.step.total_seconds()))) def __contains__(self, value): if self.stop is not None: check = ( - self.start >= value > self.stop - if self._has_neg_step - else self.start <= value < self.stop + self.start >= value > self.stop + if self._has_neg_step + else self.start <= value < self.stop ) else: check = self.start >= value if self._has_neg_step else self.start <= value @@ -750,9 +750,9 @@ def __getitem__(self, idx_or_slice): if isinstance(idx_or_slice, slice): return self._getslice(idx_or_slice) raise TypeError( - "DateRange indices must be integers or slices not {0}".format( - idx_or_slice.__class__ - ) + "DateRange indices must be integers or slices not {0}".format( + idx_or_slice.__class__ + ) ) def _getidx(self, idx): diff --git a/tests/.coveragerc b/tests/.coveragerc index e9e7678..5505d9a 100644 --- a/tests/.coveragerc +++ b/tests/.coveragerc @@ -1,10 +1,11 @@ -; References: +; .coveragerc +; +; references: ; https://coverage.readthedocs.io/ -[html] -directory = tests/reports/coverage [run] branch = True +data_file = tests/reports/.coverage omit = .eggs/* setup.py @@ -17,6 +18,7 @@ omit = venv/* [report] +show_missing = True ignore_errors = True omit = */tests/* @@ -26,3 +28,20 @@ exclude_lines = raise NotImplementedError.* pragma: NO COVER def __repr__ + +[html] +title = Coverage Report +directory = tests/reports/coverage + +[json] +output = tests/reports/coverage.json +pretty_print = True +show_contexts = True + +[lcov] +output = tests/reports/coverage.lcov +line_checksums = True + +[xml] +output = tests/reports/coverage.xml + diff --git a/tests/conftest.py b/tests/conftest.py index 974dc4d..5f38c9f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,15 +20,15 @@ from .utils import module_name as _modname __all__ = ( - "module_name", - "prepared_request", - "response", - "error", - "modname", - "filename", - "number", - "boolean", - "session" + "module_name", + "prepared_request", + "response", + "error", + "modname", + "filename", + "number", + "boolean", + "session" ) @@ -65,8 +65,8 @@ class PreparedRequest(object): """ body = b"" headers = { - "Accept": "application/json", - "Content-type": "application/json" + "Accept": "application/json", + "Content-type": "application/json" } method = "get" path_url = "?p1=param1&p2=param2" @@ -90,9 +90,9 @@ class Response(object): cookies = {} encoding = "utf8" headers = { - "Accept": "application/json", - "Content-type": "application/json", - "Content-disposition": "application/json" + "Accept": "application/json", + "Content-type": "application/json", + "Content-disposition": "application/json" } reason = "OK" status_code = 200 @@ -103,8 +103,8 @@ class PreparedRequest(object): """ body = b"" headers = { - "Accept": "application/json", - "Content-type": "application/json" + "Accept": "application/json", + "Content-type": "application/json" } method = "get" path_url = "?p1=param1&p2=param2" diff --git a/tests/test_cli.py b/tests/test_cli.py index b18e7d7..4ffc4dd 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -15,8 +15,8 @@ LOGGER = logging.getLogger(__name__) __all__ = ( - "test_arg_parser", - "test_main" + "test_arg_parser", + "test_main" ) @@ -26,7 +26,6 @@ def test_arg_parser(): parser = argparser() is_argparser = isinstance(parser, ArgumentParser) is True assert is_argparser, "Invalid argparser type: {0}".format(type(parser)) - return is_argparser def test_main(module_name): @@ -45,15 +44,14 @@ def test_main(module_name): success = False except Exception as e: LOGGER.exception( - "cli command execution: status=failed command=%s error=%r", - command, e + "cli command execution: status=failed command=%s error=%r", + command, e ) success = False else: success = True if ret == 0 else False LOGGER.debug( - "cli command execution: status=%s command=%s error=%r", - success, command, None + "cli command execution: status=%s command=%s error=%r", + success, command, None ) assert success is True, "CLI Test Failed with return_code: {0}".format(ret) - return success diff --git a/tests/test_config.py b/tests/test_config.py index 1707735..4a47c2a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -11,14 +11,14 @@ from skeleton.config import Configuration __all__ = ( - "test_config", - "test_configuration_from_env", - "test_configuration_update_from_env", - "test_configuration_from_object", - "test_configuration_dump", - "test_configuration_from_dict", - "test_configuration_merge_all", - "test_configuration_search_paths" + "test_config", + "test_configuration_from_env", + "test_configuration_update_from_env", + "test_configuration_from_object", + "test_configuration_dump", + "test_configuration_from_dict", + "test_configuration_merge_all", + "test_configuration_search_paths" ) LOGGER = logging.getLogger(__name__) @@ -35,9 +35,9 @@ def test_config(): LOGGER.debug("configuration: %s", config) - assert isinstance(config, Configuration), "Invalid configuration type: " \ - "{0}".format(type(config)) - return True + assert isinstance(config, Configuration), ( + "Invalid configuration type: {0}" + ).format(type(config).__name__) def test_configuration_from_env(): @@ -48,10 +48,9 @@ def test_configuration_from_env(): config = Configuration.from_env() LOGGER.debug( - "(from_env) Configuration: %s", - config.as_string(indent=2, sort_keys=True) + "(from_env) Configuration: %s", + config.as_string(indent=2, sort_keys=True) ) - return True def test_configuration_update_from_env(): @@ -69,10 +68,9 @@ def test_configuration_update_from_env(): assert os.environ["client_cert"] == "bbb", "Failed to update from env" LOGGER.debug( - "(update_from_env) Configuration: %s", - config.as_string(indent=2, sort_keys=True) + "(update_from_env) Configuration: %s", + config.as_string(indent=2, sort_keys=True) ) - return True def test_configuration_from_object(): @@ -88,14 +86,11 @@ class CONFIG: config = Configuration.from_object(CONFIG) LOGGER.debug( - "(from_object) Configuration: %s", - config.as_string(indent=2, sort_keys=True) + "(from_object) Configuration: %s", + config.as_string(indent=2, sort_keys=True) ) - assert config.get("client_cert") == CONFIG.CLIENT_CERT, "invalid client cert" - return True - def test_configuration_merge_all(): """Test Configuration.merge_all. @@ -110,10 +105,9 @@ def test_configuration_merge_all(): config_merged = config_base.merge_all(config_1, config_2) LOGGER.debug( - "(merged) Configuration: %s", - config_merged.as_string(indent=2, sort_keys=True) + "(merged) Configuration: %s", + config_merged.as_string(indent=2, sort_keys=True) ) - return True def test_configuration_from_dict(): @@ -122,34 +116,33 @@ def test_configuration_from_dict(): LOGGER.debug("Testing: Configuration.from_dict") config = Configuration.from_dict({ - 'client_cert': "b", - 'connect_timeout': 60, - 'log_datefmt': "%Y-%m-%d %H:%M:%S", - 'log_file': None, - 'log_filemode': "a+", - 'log_format': ( - "(%(asctime)s)[%(levelname)s]%(name)s." - "%(funcName)s(%(lineno)d):%(message)s" - ), - 'log_level': "ERROR", - 'log_style': "%", - 'max_pool_connections': 10, - 'pool_timeout': None, - 'poolblock': False, - 'poolsize': 10, - 'proxies': "C", - 'proxies_config': None, - "read_timeout": 60, - "retries": 0, - "user_agent": "skeleton-0.0.8", - "verify": False + 'client_cert': "b", + 'connect_timeout': 60, + 'log_datefmt': "%Y-%m-%d %H:%M:%S", + 'log_file': None, + 'log_filemode': "a+", + 'log_format': ( + "(%(asctime)s)[%(levelname)s]%(name)s." + "%(funcName)s(%(lineno)d):%(message)s" + ), + 'log_level': "ERROR", + 'log_style': "%", + 'max_pool_connections': 10, + 'pool_timeout': None, + 'poolblock': False, + 'poolsize': 10, + 'proxies': "C", + 'proxies_config': None, + "read_timeout": 60, + "retries": 0, + "user_agent": "skeleton-0.0.8", + "verify": False }) LOGGER.debug( - "(from_dict) Configuration: %s", - config.as_string(indent=2, sort_keys=True) + "(from_dict) Configuration: %s", + config.as_string(indent=2, sort_keys=True) ) - return True def test_configuration_dump(): @@ -159,24 +152,17 @@ def test_configuration_dump(): config = Configuration() - LOGGER.debug("Configuration: %s", config.as_string( - indent=2, sort_keys=True - )) + LOGGER.debug("Configuration: %s", config.as_string(indent=2, sort_keys=True)) s = StringIO() - config.dump(s) - config_dumped = Configuration.from_dict( - s.getvalue() - ) + config_dumped = Configuration.from_dict(s.getvalue()) LOGGER.debug( - "(Dumped) Configuration: %s", - config_dumped.as_string(indent=2, sort_keys=True) + "(Dumped) Configuration: %s", + config_dumped.as_string(indent=2, sort_keys=True) ) - assert config == config_dumped - return True def test_configuration_search_paths(): @@ -187,4 +173,3 @@ def test_configuration_search_paths(): config = Configuration() LOGGER.debug("Configuration Search Paths: %s", config.SEARCH_PATHS) - return True diff --git a/tests/test_error.py b/tests/test_error.py index 518d332..d8b39aa 100644 --- a/tests/test_error.py +++ b/tests/test_error.py @@ -10,7 +10,7 @@ from skeleton.error import BaseError __all__ = ( - "test_base_error", + "test_base_error", ) LOGGER = logging.getLogger(__name__) @@ -29,12 +29,13 @@ def test_base_error(error): args=%s kwargs=%s """, error.msg, error.kwargs, error.fmt, error.args) + try: raise error except BaseError as e: LOGGER.exception("Caught Test Exception: %s", e.json) assert e.args[0] == BaseError.fmt.format(error="TEST ERROR MESSAGE") - return True + except Exception as e: LOGGER.debug("Caught Incorrect Exception: %r", e) - return False + assert False, "Caught Incorrect Exception: {0}".format(e) diff --git a/tests/test_log.py b/tests/test_log.py index 35d1725..b8b275a 100644 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -3,6 +3,8 @@ """ from __future__ import absolute_import, print_function, unicode_literals +from unittest.mock import patch + from skeleton.log import ( apply_session_hook, log_request, @@ -11,14 +13,14 @@ ) __all__ = ( - "test_log_request", - "test_log_request_content", - "test_log_response", - "test_log_response_content", - "test_log_request_response", - "test_log_request_response_content", - "test_apply_session_hook", - "test_apply_session_hook_content" + "test_log_request", + "test_log_request_content", + "test_log_response", + "test_log_response_content", + "test_log_request_response", + "test_log_request_response_content", + "test_apply_session_hook", + "test_apply_session_hook_content" ) @@ -28,7 +30,9 @@ def test_log_request(prepared_request): Args: prepared_request (PreparedRequest): PreparedRequest Instance """ - log_request(prepared_request) + with patch("skeleton.log.logger") as mock_logger: + log_request(prepared_request) + mock_logger.info.assert_called() def test_log_request_content(prepared_request, log_content=True): @@ -37,7 +41,9 @@ def test_log_request_content(prepared_request, log_content=True): Args: prepared_request (PreparedRequest): PreparedRequest Instance """ - log_request(prepared_request, log_content=log_content) + with patch("skeleton.log.logger") as mock_logger: + log_request(prepared_request, log_content=log_content) + mock_logger.info.assert_called() def test_log_response(response): @@ -46,7 +52,9 @@ def test_log_response(response): Args: response (Response): Response Instance """ - log_response(response) + with patch("skeleton.log.logger") as mock_logger: + log_response(response) + mock_logger.info.assert_called() def test_log_response_content(response, log_content=True): @@ -55,40 +63,52 @@ def test_log_response_content(response, log_content=True): Args: response (Response): Response Instance """ - log_response(response, log_content=log_content) + with patch("skeleton.log.logger") as mock_logger: + log_response(response, log_content=log_content) + mock_logger.info.assert_called() def test_log_request_response(response, log_content=False): - """Test `skeleton.log_response` with log_content set to True. + """Test `skeleton.log_request_response` with log_content set to False. Args: response (Response): Response Instance """ - log_request_response(response, log_content=log_content) + with patch("skeleton.log.logger") as mock_logger: + log_request_response(response, log_content=log_content) + mock_logger.info.assert_called() def test_log_request_response_content(response, log_content=True): - """Test `skeleton.log_response` with log_content set to True. + """Test `skeleton.log_request_response` with log_content set to True. Args: response (Response): Response Instance """ - log_request_response(response, log_content=log_content) + with patch("skeleton.log.logger") as mock_logger: + log_request_response(response, log_content=log_content) + mock_logger.info.assert_called() def test_apply_session_hook(session, log_content=False): - """Test `skeleton.log_response` with log_content set to True. + """Test `skeleton.apply_session_hook` with log_content set to False. Args: session (requests.Session): Session Instance """ apply_session_hook(session, log_content=log_content) + assert session.hooks["response"] == [log_response], ( + "log_response was not set as a session hook" + ) def test_apply_session_hook_content(session, log_content=True): - """Test `skeleton.log_response` with log_content set to True. + """Test `skeleton.apply_session_hook` with log_content set to True. Args: session (requests.Session): Session Instance """ apply_session_hook(session, log_content=log_content) + assert session.hooks["response"] == [log_response], ( + "log_response was not set as a session hook" + ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 2550efc..8e30cea 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -16,11 +16,11 @@ ) __all__ = ( - "test_safe_b64decode", - "test_as_number", - "test_as_bool", - "test_to_valid_filename", - "test_to_valid_module_name" + "test_safe_b64decode", + "test_as_number", + "test_as_bool", + "test_to_valid_filename", + "test_to_valid_module_name" ) LOGGER = logging.getLogger(__name__) @@ -33,16 +33,15 @@ def test_safe_b64decode(): decoded = safe_b64decode(value) LOGGER.debug("input: %s output: %s", value, decoded) assert isinstance(decoded, binary_type), "invalid decoded base64 return type" - return True def test_as_number(number): """Test as_number function. """ num = as_number(number) - assert isinstance(num, integer_types), "error converting str to number: {0}" \ - "".format(number) - return num + assert isinstance(num, integer_types), ( + "error converting str to number: {0}" + ).format(number) def test_as_bool(boolean): @@ -50,7 +49,6 @@ def test_as_bool(boolean): """ num = as_bool(boolean) assert isinstance(num, bool), "error converting string to bool: " + boolean - return num def test_to_valid_filename(filename): @@ -59,7 +57,6 @@ def test_to_valid_filename(filename): valid_filename = to_valid_filename(filename) LOGGER.debug("input: %s output: %s", filename, filename) assert filename != valid_filename - return filename def test_to_valid_module_name(modname): @@ -68,4 +65,3 @@ def test_to_valid_module_name(modname): valid_module_name = to_valid_module_name(modname) LOGGER.debug("input: %s output: %s", modname, valid_module_name) assert modname != valid_module_name - return valid_module_name diff --git a/tests/utils.py b/tests/utils.py index f1bc1ef..17e8e0d 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -14,15 +14,15 @@ __file__ = os.path.abspath(__file__) # noqa __all__ = ( - "PARENT", - "ROOT", - "LOG_DIR", - "DATA_DIR", - "TEST_DIR", - "cwd", - "module_name", - "run", - "run_in_root" + "PARENT", + "ROOT", + "LOG_DIR", + "DATA_DIR", + "TEST_DIR", + "cwd", + "module_name", + "run", + "run_in_root" ) LOGGER = logging.getLogger(__name__) @@ -78,13 +78,13 @@ def _run(_command): ret = e.returncode LOGGER.exception( - "error occurred while running command: %s return_code: %s", - _command, ret + "error occurred while running command: %s return_code: %s", + _command, ret ) else: LOGGER.debug( - "successfully ran command: %s return_code: %s", - _command, ret + "successfully ran command: %s return_code: %s", + _command, ret ) return ret From 4e6d17a86708d4788ddf52c60c0240c471081afd Mon Sep 17 00:00:00 2001 From: chad8242310 <20379178-chad8242310@users.noreply.gitlab.com> Date: Thu, 22 May 2025 09:39:15 -0400 Subject: [PATCH 2/6] formatting --- setup.cfg | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/setup.cfg b/setup.cfg index 471511b..dba8d6f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,10 +8,13 @@ release = sdist bdist_wheel docs = build_sphinx [bdist_rpm] -doc_files = docs,LICENSE.txt,README.rst +doc_files = + docs + LICENSE.txt + README.rst install-script = scripts/rpm_install.sh release = 1 -packager = Chad Loether +packager = Packager [bdist_wheel] universal = 1 @@ -61,8 +64,7 @@ description-file = README.rst ;suppress inspection for section "LongLine" [pycodestyle] -# References: -# https://pycodestyle.readthedocs.io/ +# references: https://pycodestyle.readthedocs.io/ # select = # hang-closing = # quiet = @@ -72,8 +74,32 @@ description-file = README.rst max-doc-length = 75 max-line-length = 79 filename = *.py -exclude = __pycache__,docs/source/conf.py,venv*,old*,dist*,build*,*.egg-info,.idea*,.git,.tox*,*.pytest_cache*,setup.py +exclude = + __pycache__ + docs/source/conf.py + venv*,old* + dist* + build* + *.egg-info + .idea* + .git + .tox* + *.pytest_cache* + setup.py count = 1 format = pylint verbose = 1 -ignore = E111,E114,E121,E129,E402,E501,E741,E722,F401,F403,F405,W503,W504 +ignore = + E111 + E114 + E121 + E129 + E402 + E501 + E741 + E722 + F401 + F403 + F405 + W503 + W504 From cfa0b14d4105daad3065ed3b8abee71795a45829 Mon Sep 17 00:00:00 2001 From: chad8242310 <20379178-chad8242310@users.noreply.gitlab.com> Date: Thu, 22 May 2025 09:40:56 -0400 Subject: [PATCH 3/6] python versions tox --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 767c69c..9adde62 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,9 @@ python: - "3.7" - "3.8" - "3.9" + - "3.10" + - "3.11" + - "3.12" install: pip install tox-travis script: tox -after_success: pycodestyle +after_success: pycodestyle \ No newline at end of file From b7af722158bd05c19fbc46c2ff5a9727bc1af178 Mon Sep 17 00:00:00 2001 From: chad8242310 <20379178-chad8242310@users.noreply.gitlab.com> Date: Thu, 22 May 2025 09:50:17 -0400 Subject: [PATCH 4/6] updated test runner --- .gitignore | 5 +++-- scripts/runtests.py | 50 ++++++++++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 80e496b..f9e374b 100644 --- a/.gitignore +++ b/.gitignore @@ -135,7 +135,8 @@ dmypy.json # MacOS .DS_Store -# Testing Directories +# Testing Files and Directories +tests/*.log tests/.pytest_cache/ tests/reports/ -tests/*.log +tests/logs/ \ No newline at end of file diff --git a/scripts/runtests.py b/scripts/runtests.py index 6db361a..84c20c4 100755 --- a/scripts/runtests.py +++ b/scripts/runtests.py @@ -12,6 +12,7 @@ """ from __future__ import absolute_import, print_function, unicode_literals +import logging import os from contextlib import contextmanager from errno import EEXIST @@ -96,6 +97,23 @@ def mkdir_p(path): raise +def mkdirs_p(*path): + """Create multiple directories. + + Notes: + Unix "mkdir -p" equivalent. + + Args: + path (str): Filepaths to create. + + Raises: + OSError: Raised for exceptions unrelated to the + directory already existing. + """ + for p in path: + mkdir_p(p) + + def touch(filepath): """Equivalent of Unix `touch` command. @@ -118,37 +136,31 @@ def main(): Returns: int: Command return code. """ + logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(levelname)s %(message)s") + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) module = module_name(where=repo_root) + logging.debug("running tests for module: %s", module) return_code = -1 # noqa + with cwd(repo_root): + # noinspection PyUnusedLocal env_name = os.getenv("ENVNAME", "test") tests_dir = os.path.join(repo_root, "tests") logs_dir = os.path.join(tests_dir, "logs") - mkdir_p(logs_dir) - + reports_dir = os.path.join(tests_dir, "reports") tests_log_file = os.path.join(logs_dir, "pytest.log") + + mkdirs_p(logs_dir, reports_dir) touch(tests_log_file) # prevent pytest error due to missing log file - reports_dir = os.path.join(tests_dir, "reports") - mkdir_p(reports_dir) - - tests_html_filename = "{0!s}.html".format(env_name) - tests_html_file = os.path.join(reports_dir, tests_html_filename) - - return_code = run( - "pytest {posargs} " - "--cov={module} " - "--html={tests_html_file} " - "--self-contained-html".format( - module=module, - tests_html_file=tests_html_file, - envname=env_name, - posargs=tests_dir - ) - ) + return_code = run("pytest {posargs} --cov={module}".format( + posargs=tests_dir, + module=module + )) + return return_code From 0d44ebdebb43164a91ac91fad1320ff5849739d7 Mon Sep 17 00:00:00 2001 From: chad8242310 <20379178-chad8242310@users.noreply.gitlab.com> Date: Thu, 22 May 2025 10:25:26 -0400 Subject: [PATCH 5/6] docs update --- README.md | 6 ++-- README.rst | 66 ++++++++++++++++++---------------- docs/source/conf.py | 77 +++++++++++++++++++++++++++++++++++----- docs/source/index.rst | 66 ++++++++++++++++++---------------- docs/source/skeleton.rst | 14 ++++---- scripts/apidoc.py | 53 +++++++++++++++++++++++++-- skeleton/__version__.py | 8 ++--- 7 files changed, 201 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index fa059b6..9ddca6f 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,8 @@ Install documentation dependencies and build: ## License -MIT - See -[LICENSE](https://github.com/cloether/skeleton/blob/master/LICENSE.txt) -for more information. +MIT - See [LICENSE](https://github.com/cloether/skeleton/blob/master/LICENSE.txt) for more information. ## Copyright -Copyright © 2021 Chad Loether +Copyright © 2025 cloether diff --git a/README.rst b/README.rst index 4a49713..e553012 100644 --- a/README.rst +++ b/README.rst @@ -1,65 +1,69 @@ -.. - https://docutils.sourceforge.io/docs/user/rst/quickref.html - https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet - Skeleton ======== ``skeleton`` is a template used for creating Python modules. -The official repo is `here`_. +The official repo is `here `__. - .. note:: This library is under active development. + **Note**: This library is under active development. Installation ------------ -To install the library into the current virtual environment:: - $ pip install skeleton +Install in virtual environment: -or:: +:: - $ python -m pip install skeleton + $ pip install skeleton Running the Tests ----------------- -Install test dependencies and run `tox`:: - $ pip install skeleton[tests] - $ tox +Install test dependencies and run ``tox``: + +:: + + $ pip install skeleton[tests] + $ tox Building the Docs ----------------- -Install documentation dependencies and build:: - $ pip install skeleton[docs] - $ python setup.py docs +Install documentation dependencies and build: + +:: + + $ pip install skeleton[docs] + $ python setup.py docs Requirements ------------ + - six - appdirs Support ------- -- Ask a `Question`_. -- Request `Assistance`_. -- Submit `Bug Report`_. -- Submit `Feature Request`_. -- Submit `Issue`_. + +- Ask a + `Question `__ +- Request + `Assistance `__ +- Submit `Bug + Report `__ +- Submit `Feature + Request `__ +- Submit + `Issue `__ License ------- -MIT - See `LICENSE`_ for more information. + +MIT - See +`LICENSE `__ +for more information. Copyright --------- -Copyright © 2021 Chad Loether - -.. _here: https://github.com/cloether/skeleton -.. _Issue: https://github.com/cloether/skeleton/issues/new?template=blank-issue.md -.. _Bug Report: https://github.com/cloether/skeleton/issues/new?template=bug-report.md&labels=bug -.. _Feature Request: https://github.com/cloether/skeleton/issues/new?template=feature-request.md&labels=enhancement -.. _Question: https://github.com/cloether/skeleton/issues/new?template=question.md&labels=question -.. _Assistance: https://github.com/cloether/skeleton/issues/new?template=need-help.md&labels=help+wanted -.. _LICENSE: https://github.com/cloether/skeleton/blob/master/LICENSE.txt + +Copyright © 2025 cloether diff --git a/docs/source/conf.py b/docs/source/conf.py index 16efd1e..33206e7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -9,22 +9,81 @@ from __future__ import absolute_import, print_function, unicode_literals import os +import runpy import sys import time +from setuptools import find_packages + # -- Path setup ----------------------------------------------------- # If extensions (or modules to document with autodoc) are in another # directory, add these directories to sys.path here. If the directory # is relative to the documentation root, use os.path.abspath to make # it absolute, like shown here. -sys.path.insert(0, os.path.abspath(__file__)) -from skeleton.__version__ import ( - __author__, - __title__, - __version__, - __description__ -) # noqa +project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) + +sys.path.insert(0, os.path.abspath(__file__)) +sys.path.insert(0, project_root) + + +def module_name(exclude=("docs*", "examples*", "scripts*", "tests*", "build*", "venv*"), + where=".", include=("*",), default=None): + """Infer the top-level module name for the current project. + + Returns: + str: First matching module name, or `default` if none found. + """ + packages = find_packages(where=where, include=include, exclude=exclude) + return next(iter(sorted(packages)), default) + + +def load_metadata_from_version_file(root=None, module=None): + """Loads metadata from a __version__.py file somewhere under the project root. + + Args: + root (str): Path to the root of the project. If None, the current directory is used. + module (str): Name of the module to load. If None, the first found module is used. + + Returns: + dict: { + '__title__': str, + '__author__': str, + '__version__': str, + '__description__': str + } + """ + if root is None: + root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) + + if module is None: + module = module_name(where=root) + + version_file = os.path.join(root, module, "__version__.py") + + if not os.path.isfile(version_file): + # Search for any __version__.py + for root, dirs, files in os.walk(root): + if "__version__.py" in files: + version_file = os.path.join(root, "__version__.py") + break + else: + raise FileNotFoundError("Could not locate __version__.py in project.") + + metadata = runpy.run_path(version_file) + return { + "__title__": metadata.get("__title__", os.path.basename(root)), + "__author__": metadata.get("__author__", "Unknown"), + "__version__": metadata.get("__version__", "0.0.0"), + "__description__": metadata.get("__description__", ""), + } + + +__meta__ = load_metadata_from_version_file(root=project_root) +__title__ = __meta__["__title__"] +__author__ = __meta__["__author__"] +__version__ = __meta__["__version__"] +__description__ = __meta__["__description__"] # -- Project information -------------------------------------------- @@ -33,7 +92,7 @@ # noinspection PyShadowingBuiltins copyright = "{0}, {1}".format(author, time.strftime("%Y")) version = "{0}.".format(__version__.split(".")[:-1]) # Short X.Y version. -release = __version__ # Full version, including alpha/beta/rc tags +release = __version__ # full version, including alpha/beta/rc tags # -- General configuration ------------------------------------------ @@ -50,7 +109,7 @@ "sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.todo", - "sphinxcontrib.napoleon", + "sphinx.ext.napoleon", "guzzle_sphinx_theme" ] diff --git a/docs/source/index.rst b/docs/source/index.rst index 73f7347..2779ec1 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,68 +1,72 @@ -.. - https://docutils.sourceforge.io/docs/user/rst/quickref.html - https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet - Skeleton ======== ``skeleton`` is a template used for creating Python modules. -The official repo is `here`_. +The official repo is `here `__. - .. note:: This library is under active development. + **Note**: This library is under active development. Installation ------------ -To install the library into the current virtual environment:: - $ pip install skeleton +Install in virtual environment: -or:: +:: - $ python -m pip install skeleton + $ pip install skeleton Running the Tests ----------------- -Install test dependencies and run `tox`:: - $ pip install skeleton[tests] - $ tox +Install test dependencies and run ``tox``: + +:: + + $ pip install skeleton[tests] + $ tox Building the Docs ----------------- -Install documentation dependencies and build:: - $ pip install skeleton[docs] - $ python setup.py docs +Install documentation dependencies and build: + +:: + + $ pip install skeleton[docs] + $ python setup.py docs Requirements ------------ + - six - appdirs Support ------- -- Ask a `Question`_. -- Request `Assistance`_. -- Submit `Bug Report`_. -- Submit `Feature Request`_. -- Submit `Issue`_. + +- Ask a + `Question `__ +- Request + `Assistance `__ +- Submit `Bug + Report `__ +- Submit `Feature + Request `__ +- Submit + `Issue `__ License ------- -MIT - See `LICENSE`_ for more information. + +MIT - See +`LICENSE `__ +for more information. Copyright --------- -Copyright © 2021 Chad Loether - -.. _here: https://github.com/cloether/skeleton -.. _Issue: https://github.com/cloether/skeleton/issues/new?template=blank-issue.md -.. _Bug Report: https://github.com/cloether/skeleton/issues/new?template=bug-report.md&labels=bug -.. _Feature Request: https://github.com/cloether/skeleton/issues/new?template=feature-request.md&labels=enhancement -.. _Question: https://github.com/cloether/skeleton/issues/new?template=question.md&labels=question -.. _Assistance: https://github.com/cloether/skeleton/issues/new?template=need-help.md&labels=help+wanted -.. _LICENSE: https://github.com/cloether/skeleton/blob/master/LICENSE.txt + +Copyright © 2025 cloether Reference ========= diff --git a/docs/source/skeleton.rst b/docs/source/skeleton.rst index 33d6437..3c10b07 100644 --- a/docs/source/skeleton.rst +++ b/docs/source/skeleton.rst @@ -9,53 +9,53 @@ skeleton.cli module .. automodule:: skeleton.cli :members: - :undoc-members: :show-inheritance: + :undoc-members: skeleton.config module ---------------------- .. automodule:: skeleton.config :members: - :undoc-members: :show-inheritance: + :undoc-members: skeleton.const module --------------------- .. automodule:: skeleton.const :members: - :undoc-members: :show-inheritance: + :undoc-members: skeleton.error module --------------------- .. automodule:: skeleton.error :members: - :undoc-members: :show-inheritance: + :undoc-members: skeleton.log module ------------------- .. automodule:: skeleton.log :members: - :undoc-members: :show-inheritance: + :undoc-members: skeleton.utils module --------------------- .. automodule:: skeleton.utils :members: - :undoc-members: :show-inheritance: + :undoc-members: Module contents --------------- .. automodule:: skeleton :members: - :undoc-members: :show-inheritance: + :undoc-members: diff --git a/scripts/apidoc.py b/scripts/apidoc.py index 3a28cd0..a33cd32 100755 --- a/scripts/apidoc.py +++ b/scripts/apidoc.py @@ -149,6 +149,10 @@ def module_name(exclude=("doc*", "example*", "script*", "test*"), where=".", def docs_generate(parser, args): """Generate Project Documentation Files using sphinx-apidoc. + Args: + parser (argparse.ArgumentParser): Argument parser. + args (argparse.Namespace): Parsed arguments. + Returns: int: Sphinx command return code. """ @@ -169,6 +173,37 @@ def docs_build(parser, args): return run("make html", os.path.abspath(os.path.join(repo_root, "docs"))) +# noinspection PyUnusedLocal +def readme_to_rst(parser, args): + """Convert README.md to README.rst using pandoc. + + Args: + parser (argparse.ArgumentParser): Argument parser. + args (argparse.Namespace): Parsed arguments. + + Returns: + int: Sphinx command return code. + """ + repo_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) + readme_md = os.path.join(repo_root, "README.md") + readme_rst = os.path.join(repo_root, "README.rst") + return run("pandoc -f markdown -t rst -o {0} {1}".format(readme_rst, readme_md)) + + +def convert_update_readme(parser, args): + """Convert README.md to README.rst and update index.rst. + + Args: + parser (argparse.ArgumentParser): Argument parser. + args (argparse.Namespace): Parsed arguments. + + Returns: + int: Sphinx command return code. + """ + readme_to_rst(parser, args) + return docs_update(parser, args) + + # noinspection PyUnusedLocal def docs_update(parser, args): """Update index.rst to match the project README.rst @@ -254,13 +289,25 @@ def main(**kwargs): ) update_parser.set_defaults(func=docs_update, command="update") + convert_parser = sub.add_parser( + "convert", + add_help=False, + help="convert README.md to README.rst" + ) + convert_parser.set_defaults(func=readme_to_rst, command="convert") + + convert_update_parser = sub.add_parser( + "convert-update", + add_help=False, + help="convert README.md to README.rst and update index.rst" + ) + convert_update_parser.set_defaults(func=convert_update_readme, command="convert-update") + parser.set_defaults(func=docs_build, command="build") args = parser.parse_args() - logging.basicConfig( - level=logging.DEBUG if args.debug else logging.CRITICAL - ) + logging.basicConfig(level=logging.DEBUG if args.debug else logging.CRITICAL) return args.func(parser, args) diff --git a/skeleton/__version__.py b/skeleton/__version__.py index d727451..37e77a0 100644 --- a/skeleton/__version__.py +++ b/skeleton/__version__.py @@ -21,11 +21,11 @@ "__version__", ) -__author__ = "Chad Loether" -__author_email__ = "chad.loether@outlook.com" -__copyright__ = "Copyright 2021 Chad Loether" +__author__ = "cloether" +__author_email__ = "20920516+cloether@users.noreply.github.com" +__copyright__ = "Copyright 2025 Chad Loether" __description__ = "Python Module Template" __title__ = "skeleton" __license__ = "MIT" __url__ = "https://github.com/cloether/skeleton" -__version__ = "0.0.10" +__version__ = "0.0.11" From e21df48e600475963c9f658547b9b4203e9371c3 Mon Sep 17 00:00:00 2001 From: chad8242310 <20379178-chad8242310@users.noreply.gitlab.com> Date: Thu, 22 May 2025 11:06:57 -0400 Subject: [PATCH 6/6] scripts --- .../enhancement-documentation-19879.json | 5 + pyproject.toml | 5 +- scripts/change.py | 2 +- scripts/checkenc.py | 5 +- scripts/checklic.py | 6 +- scripts/install.py | 2 + scripts/{rstlint.py => lint.py} | 96 +++++++- setup.cfg | 10 +- setup.py | 228 +++++++++--------- 9 files changed, 221 insertions(+), 138 deletions(-) create mode 100644 .github/.changes/next-release/enhancement-documentation-19879.json rename scripts/{rstlint.py => lint.py} (74%) diff --git a/.github/.changes/next-release/enhancement-documentation-19879.json b/.github/.changes/next-release/enhancement-documentation-19879.json new file mode 100644 index 0000000..682ebef --- /dev/null +++ b/.github/.changes/next-release/enhancement-documentation-19879.json @@ -0,0 +1,5 @@ +{ + "type": "enhancement", + "category": "documentation", + "description": "updated documentation generation" +} diff --git a/pyproject.toml b/pyproject.toml index a6f9b76..23f2151 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ build-backend = "setuptools.build_meta" [project] name = "skeleton" -version = "0.0.10" +version = "0.0.11" description = "Python Module Template" readme = "README.md" license = { text = "MIT" } @@ -89,8 +89,7 @@ tests = [ "twine", "wheel" ] -python2.6 = ["ordereddict==1.1", "simplejson==3.3.0"] -python2.7 = ["ipaddress"] +python2 = ["ordereddict==1.1", "simplejson==3.3.0", "ipaddress"] [project.scripts] skeleton = "skeleton.__main__:main" \ No newline at end of file diff --git a/scripts/change.py b/scripts/change.py index 393115c..b5d75ff 100755 --- a/scripts/change.py +++ b/scripts/change.py @@ -52,7 +52,7 @@ _VALID_CHARS = set(string.ascii_letters + string.digits) _ROOT_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) -CHANGE_DIR = os.path.abspath(os.path.join(_ROOT_DIR, '.changes')) +CHANGE_DIR = os.path.abspath(os.path.join(_ROOT_DIR, '.github', '.changes')) CHANGE_TYPES = ('bugfix', 'feature', 'enhancement', 'api-change') CHANGE_TEMPLATE = """\ # Type should be one of: feature, bugfix, enhancement, api-change diff --git a/scripts/checkenc.py b/scripts/checkenc.py index 8de4cf3..f9e003f 100755 --- a/scripts/checkenc.py +++ b/scripts/checkenc.py @@ -3,6 +3,9 @@ """checkenc.py Find Python files that are missing a coding directive. + +Example Usage: + python scripts/checkenc.py *.py skeleton/*.py docs/source/*.py scripts/*.py tests/*.py --debug """ from __future__ import absolute_import, print_function, unicode_literals @@ -15,7 +18,7 @@ from six import ensure_str, text_type -__version__ = "0.0.2" +__version__ = "0.0.3" LOGGER = logging.getLogger(__name__) diff --git a/scripts/checklic.py b/scripts/checklic.py index 7e2694b..c6cd05a 100755 --- a/scripts/checklic.py +++ b/scripts/checklic.py @@ -32,11 +32,12 @@ # ------------------------------------------------------------------------------ """ -ENV_DIRS = frozenset([ +ENV_DIRS = frozenset(filter(None, [ os.path.join(ROOT_DIR, 'env'), os.path.join(ROOT_DIR, 'env27'), os.path.join(ROOT_DIR, '.tox'), -]) + os.getenv('VIRTUAL_ENV', 'venv'), +])) def contains_header(text): @@ -66,6 +67,7 @@ def get_files_without_header(extensions=('.py',), root_dir=ROOT_DIR, """ files_without_header = [] for dirpath, _, filenames in os.walk(root_dir): + filenames.sort() # skip folders generated by virtual env if any(d for d in env_dirs if d in dirpath): continue diff --git a/scripts/install.py b/scripts/install.py index 66f4d2e..b43b82a 100755 --- a/scripts/install.py +++ b/scripts/install.py @@ -82,6 +82,8 @@ def run_factory(directory): return partial(run, directory=directory) +# TODO: argment parser + def main(): """CLI Entry Point """ diff --git a/scripts/rstlint.py b/scripts/lint.py similarity index 74% rename from scripts/rstlint.py rename to scripts/lint.py index 194f754..3a078a2 100755 --- a/scripts/rstlint.py +++ b/scripts/lint.py @@ -32,6 +32,9 @@ LOGGER = logging.getLogger(__name__) +PARENT_DIR = os.path.dirname(os.path.realpath(__file__)) +ROOT_DIR = os.path.abspath(os.path.join(PARENT_DIR, '..')) + CHECKERS = {} CHECKER_PROPS = {"severity": 1, "falsepositives": False} @@ -155,6 +158,19 @@ ) ) +ENV_DIRS = frozenset(filter(None, [ + os.path.join(ROOT_DIR, 'env'), + os.path.join(ROOT_DIR, 'env27'), + os.path.join(ROOT_DIR, '.tox'), + os.getenv('VIRTUAL_ENV', None), + os.path.join(ROOT_DIR, 'venv'), + os.path.join(ROOT_DIR, '.venv'), + os.path.join(ROOT_DIR, '.idea'), + os.path.join(ROOT_DIR, '.git'), + os.path.join(ROOT_DIR, '.github'), + "__pycache__" +])) + def checker(*suffixes, **kwargs): """Decorator to register a function as a checker. @@ -262,7 +278,7 @@ def check_leaked_markup(_, lines): yield lno + 1, "possibly leaked markup: {0!r}".format(line) -def rstlint(path, false_pos=False, ignore=None, severity=1, verbose=False): +def lint(path, false_pos=False, ignore=ENV_DIRS, severity=1, verbose=False): """Check for stylistic and formal issues in .rst and .py files included in the documentation. @@ -277,26 +293,55 @@ def rstlint(path, false_pos=False, ignore=None, severity=1, verbose=False): (defaultdict of int,int): Severity and Error Hit Count. """ count = defaultdict(int) + totals = { + "checked": 0, + "skipped": 0, + "ignored": 0, + "errors": 0, + "total": 0 + } + for root, dirs, files in os.walk(path): + # ignore subdirectories in ignore list if abspath(root) in ignore: + if verbose: + sys.stdout.write("[-] IGNORING: {0}\n".format(root)) del dirs[:] + totals["ignored"] += 1 continue + if os.path.basename(os.path.dirname(abspath(root))) in ignore: + if verbose: + sys.stdout.write("[-] IGNORING: {0}\n".format(root)) + del dirs[:] + totals["total"] += 1 + continue + + files.sort() for fn in files: + totals["total"] += 1 + fn = join(root, fn) if fn[:2] == "./": fn = fn[2:] if abspath(fn) in ignore: + if verbose: + sys.stdout.write("[-] IGNORING: {0}\n".format(fn)) + totals["ignored"] += 1 continue # ignore files in ignore list checker_list = CHECKERS.get(splitext(fn)[1], None) if not checker_list: + if verbose: + sys.stdout.write("[-] SKIPPING: {0}\n".format(fn)) + totals["skipped"] += 1 continue if verbose: - sys.stdout.write("[-] CHECKING: {0}...\n".format(fn)) + sys.stdout.write("[-] CHECKING: {0}\n".format(fn)) + totals["checked"] += 1 # LOGGER.debug("checking: %s", fn) @@ -307,6 +352,7 @@ def rstlint(path, false_pos=False, ignore=None, severity=1, verbose=False): # LOGGER.error("%s cannot open %s", fn, err) sys.stderr.write("[!] ERROR: {0}: cannot open: {1}\n".format(fn, err)) count[4] += 1 + totals["errors"] += 1 continue for _checker in checker_list: @@ -315,24 +361,29 @@ def rstlint(path, false_pos=False, ignore=None, severity=1, verbose=False): c_sev = _checker.severity if c_sev >= severity: for n, msg in _checker(fn, lines): - sys.stdout.write( - "[{0:d}] PROBLEMS: {1}:{2:d}: {3}\n".format(c_sev, fn, n, msg) - ) + sys.stdout.write("[{0:d}] PROBLEMS: {1}:{2:d}: {3}\n".format(c_sev, fn, n, msg)) count[c_sev] += 1 if not count: if severity > 1: - sys.stdout.write( - "No Problems With Severity >= {0:d} Found.\n".format(severity) - ) + sys.stdout.write("[-] No Problems With Severity >= {0:d} Found.\n".format(severity)) else: - sys.stdout.write("No Problems Found.\n") + sys.stdout.write("[-] No Problems Found.\n") else: for severity in sorted(count): number = count[severity] sys.stdout.write("{0:d} Problem{1} With Severity {2:d} Found.\n".format( number, "s" if number > 1 else "", severity )) + + if verbose: + sys.stdout.write("\n[-] Summary:\n") + sys.stdout.write(" Total: {0}\n".format(totals["total"])) + sys.stdout.write(" Checked: {0}\n".format(totals["checked"])) + sys.stdout.write(" Skipped: {0}\n".format(totals["skipped"])) + sys.stdout.write(" Ignored: {0}\n".format(totals["ignored"])) + sys.stdout.write(" Errors: {0}\n".format(totals["errors"])) + sys.stdout.write(os.linesep) return count @@ -348,7 +399,7 @@ def _parse_args(argv): arg_d = { "verbose": False, "severity": 1, - "ignore": [], + "ignore": ENV_DIRS, "false_pos": False, } for opt, val in opts: @@ -370,9 +421,28 @@ def _parse_args(argv): return 2 if not exists(arg_d["path"]): - sys.stderr.write("ERROR: path {0} does not exist\n".format(arg_d["path"])) + sys.stderr.write("[!] ERROR: path {0} does not exist\n".format(arg_d["path"])) return 2 + if arg_d["verbose"]: + sys.stdout.write("[-] ARGUMENTS:\n") + sys.stdout.write(" path: {0}\n".format(arg_d["path"])) + sys.stdout.write(" severity: {0}\n".format(arg_d["severity"])) + sys.stdout.write(" false positives: {0}\n".format(arg_d["false_pos"])) + sys.stdout.write(" ignore: \n") + for path in arg_d["ignore"]: + sys.stdout.write(" {0}\n".format(path)) + sys.stdout.write(" verbose: {0}\n".format(arg_d["verbose"])) + sys.stdout.write(os.linesep) + + sys.stdout.write("[-] CHECKERS:\n") + for suffix, checkers in CHECKERS.items(): + sys.stdout.write(" {0} ({1})\n".format(suffix, len(checkers))) + for _checker in checkers: + sys.stdout.write(" {0}: severity={1} falsepositives={2}\n".format( + _checker.__name__, _checker.severity, _checker.falsepositives + )) + sys.stdout.write(os.linesep) return arg_d @@ -381,6 +451,8 @@ def main(): """ import signal + logging.basicConfig(level=logging.INFO) + def _shutdown_handler(signum, _): """Handle Shutdown. @@ -405,7 +477,7 @@ def _shutdown_handler(signum, _): if args_dict["verbose"]: logging.basicConfig(level=logging.DEBUG) - return int(bool(rstlint(**args_dict))) + return int(bool(lint(**args_dict))) if __name__ == "__main__": diff --git a/setup.cfg b/setup.cfg index dba8d6f..a08d428 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,7 +12,7 @@ doc_files = docs LICENSE.txt README.rst -install-script = scripts/rpm_install.sh +install_script = scripts/rpm_install.sh release = 1 packager = Packager @@ -20,8 +20,8 @@ packager = Packager universal = 1 [build_sphinx] -build-dir = docs/build -source-dir = docs/source +build_dir = docs/build +source_dir = docs/source [check-manifest] ignore = .travis.yml @@ -59,8 +59,8 @@ exclude_lines = def __repr__ [metadata] -license-files = LICENSE.txt -description-file = README.rst +license_files = LICENSE.txt +description_file = README.rst ;suppress inspection for section "LongLine" [pycodestyle] diff --git a/setup.py b/setup.py index 3ca85b5..655bceb 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ ROOT = path.abspath(path.dirname(__file__)) MODULE_META_RE = re.compile( - r"^__(?P.*)__ = ['\"](?P[^'\"]*)['\"]", re.M + r"^__(?P.*)__ = ['\"](?P[^'\"]*)['\"]", re.M ) DEFAULT_LICENSE = "MIT" @@ -93,9 +93,9 @@ def _readme_content_type(_filename, _default=None): for ext in extensions: filename = ( - basename - if not ext or ext is None - else "{0}.{1}".format(basename, ext) + basename + if not ext or ext is None + else "{0}.{1}".format(basename, ext) ) filepath = path.join(ROOT, filename) @@ -206,58 +206,58 @@ def __readlines(filepath, **kwargs): AUTHOR_EMAIL = METADATA.get("author_email") CLASSIFIERS = [ - "Development Status :: 2 - Pre-Alpha", - "Natural Language :: English", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Topic :: Software Development :: Libraries :: Python Modules" + "Development Status :: 2 - Pre-Alpha", + "Natural Language :: English", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Topic :: Software Development :: Libraries :: Python Modules" ] DESCRIPTION = METADATA.get("description") ENTRY_POINTS = { - "console_scripts": [ - "{0}={0}.__main__:main".format(NAME) - ] + "console_scripts": [ + "{0}={0}.__main__:main".format(NAME) + ] } EXTRAS_REQUIRE = { - "docs": [ - "sphinx", - "sphinxcontrib-napoleon", - "guzzle_sphinx_theme" - ], - "tests": [ - "check-manifest", - "coverage", - "pycodestyle", - "pytest", - "pytest-cov", - "pytest-html", - "tox", - "tox-travis", - "twine", - "wheel" - ], - ":python_version==\"2.6\"": [ - "ordereddict==1.1", - "simplejson==3.3.0" - ], - ":python_version==\"2.7\"": [ - "ipaddress" - ], + "docs": [ + "sphinx", + "sphinxcontrib-napoleon", + "guzzle_sphinx_theme" + ], + "tests": [ + "check-manifest", + "coverage", + "pycodestyle", + "pytest", + "pytest-cov", + "pytest-html", + "tox", + "tox-travis", + "twine", + "wheel" + ], + ":python_version==\"2.6\"": [ + "ordereddict==1.1", + "simplejson==3.3.0" + ], + ":python_version==\"2.7\"": [ + "ipaddress" + ], } INCLUDE_PACKAGE_DATA = False @@ -283,37 +283,37 @@ def __readlines(filepath, **kwargs): ZIP_SAFE = False PROJECT_URLS = { - # TODO: support more than github-based repositories. - # - bitbucket - # - gitlab - # - etc... - "Source": URL, - "Tracker": "{0}/issues".format(URL) + # TODO: support more than github-based repositories. + # - bitbucket + # - gitlab + # - etc... + "Source": URL, + "Tracker": "{0}/issues".format(URL) } # setup options setup_options = dict( - author=AUTHOR, - author_email=AUTHOR_EMAIL, - classifiers=CLASSIFIERS, - description=DESCRIPTION, - entry_points=ENTRY_POINTS, - extras_require=EXTRAS_REQUIRE, - include_package_data=INCLUDE_PACKAGE_DATA, - install_requires=REQUIREMENTS, - keywords=KEYWORDS, - license=LICENSE, - long_description=LONG_DESCRIPTION, - long_description_content_type=LONG_DESCRIPTION_CONTENT_TYPE, - name=NAME, - packages=PACKAGES, - package_data=PACKAGE_DATA, - platforms=PLATFORMS, - project_urls=PROJECT_URLS, - scripts=SCRIPTS, - url=URL, - version=VERSION, - zip_safe=ZIP_SAFE + author=AUTHOR, + author_email=AUTHOR_EMAIL, + classifiers=CLASSIFIERS, + description=DESCRIPTION, + entry_points=ENTRY_POINTS, + extras_require=EXTRAS_REQUIRE, + include_package_data=INCLUDE_PACKAGE_DATA, + install_requires=REQUIREMENTS, + keywords=KEYWORDS, + license=LICENSE, + long_description=LONG_DESCRIPTION, + long_description_content_type=LONG_DESCRIPTION_CONTENT_TYPE, + name=NAME, + packages=PACKAGES, + package_data=PACKAGE_DATA, + platforms=PLATFORMS, + project_urls=PROJECT_URLS, + scripts=SCRIPTS, + url=URL, + version=VERSION, + zip_safe=ZIP_SAFE ) # compile module as executable @@ -324,49 +324,49 @@ def __readlines(filepath, **kwargs): # py2exe specific options setup_options["options"] = { - "py2exe": { - "optimize": 0, - "skip_archive": True, - "dll_excludes": [ - "crypt32.dll" - ], - "packages": [ - "docutils", - "urllib", - "httplib", - "HTMLParser", - NAME, - "ConfigParser", - "xml.etree", - "pipes" - ], - } + "py2exe": { + "optimize": 0, + "skip_archive": True, + "dll_excludes": [ + "crypt32.dll" + ], + "packages": [ + "docutils", + "urllib", + "httplib", + "HTMLParser", + NAME, + "ConfigParser", + "xml.etree", + "pipes" + ], + } } setup_options["console"] = [ - path.join("bin", NAME) + path.join("bin", NAME) ] # run setup setup( - author=AUTHOR, - author_email=AUTHOR_EMAIL, - classifiers=CLASSIFIERS, - description=DESCRIPTION, - entry_points=ENTRY_POINTS, - extras_require=EXTRAS_REQUIRE, - include_package_data=INCLUDE_PACKAGE_DATA, - install_requires=REQUIREMENTS, - keywords=KEYWORDS, - license=LICENSE, - long_description=LONG_DESCRIPTION, - long_description_content_type=LONG_DESCRIPTION_CONTENT_TYPE, - name=NAME, - packages=PACKAGES, - package_data=PACKAGE_DATA, - platforms=PLATFORMS, - project_urls=PROJECT_URLS, - scripts=SCRIPTS, - url=URL, - version=VERSION, - zip_safe=ZIP_SAFE + author=AUTHOR, + author_email=AUTHOR_EMAIL, + classifiers=CLASSIFIERS, + description=DESCRIPTION, + entry_points=ENTRY_POINTS, + extras_require=EXTRAS_REQUIRE, + include_package_data=INCLUDE_PACKAGE_DATA, + install_requires=REQUIREMENTS, + keywords=KEYWORDS, + license=LICENSE, + long_description=LONG_DESCRIPTION, + long_description_content_type=LONG_DESCRIPTION_CONTENT_TYPE, + name=NAME, + packages=PACKAGES, + package_data=PACKAGE_DATA, + platforms=PLATFORMS, + project_urls=PROJECT_URLS, + scripts=SCRIPTS, + url=URL, + version=VERSION, + zip_safe=ZIP_SAFE )