diff --git a/.gitignore b/.gitignore index ecb2893..527f6bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,105 @@ -*.py[cdo] +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ build/ +develop-eggs/ dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ *.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ .coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio + +*.iml + +## Directory-based project format: +.idea/ +# if you remove the above rule, at least ignore the following: + +# User-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/dictionaries + +# Sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml +# .idea/uiDesigner.xml + +# Gradle: +# .idea/gradle.xml +# .idea/libraries + +# Mongo Explorer plugin: +# .idea/mongoSettings.xml + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties diff --git a/README.md b/README.md index ccead75..96fc223 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,33 @@ -# python-codacycov +# python-coverage-coverage -Submit Python `coverage` report to [Codacy](https://www.codacy.com/). +Credits to Ryan for creating this! Python coverage reporter for Codacy https://www.codacy.com -DISCLAIMER: This is an unofficial project, and is not endorsed by or -associated with Codacy in any way. +[![Build Status](https://circleci.com/gh/codacy/python-codacy-coverage.png?style=shield&circle-token=:circle-token)](https://circleci.com/gh/codacy/python-codacy-coverage) +[![Codacy Badge](https://www.codacy.com/project/badge/3a8cf06a9db94d0ab3d55e0357bc8f9d)](https://www.codacy.com/app/Codacy/python-codacy-coverage) + +## Setup + +Codacy assumes that coverage is previously configured for your project. + +You can install the coverage reporter by running: + +### Install python-codacy-coverage +``` +pip install codacy-coverage +``` + +## Updating Codacy + +To update Codacy, you will need your project API token. You can find the token in Project -> Settings -> Integrations -> Project API. + +Then set it in your terminal, replacing %Project_Token% with your own token: + +``` +export CODACY_PROJECT_TOKEN=%Project_Token% +``` + +Next, simply run the Codacy reporter. It will find the current commit and send all details to your project dashboard: + +``` +python-codacy-coverage -f coverage.xml +``` diff --git a/codacycov.py b/codacycov.py deleted file mode 100644 index f10facf..0000000 --- a/codacycov.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Submit Python coverage report to Codacy.""" - -import sys -from xml.dom import minidom - -import requests - - -XML_DOC = 'coverage.xml' -URL = 'https://www.codacy.com/api/coverage/{token}/{commit}' - - -def main(token, commit, xml_file=XML_DOC): - """Parse XML file and POST it to the Codacy API""" - - # Convert decimal string to floored int percent value - percent = lambda s: int(float(s)*100) - - # Parse the XML into the format expected by the API - xmldoc = minidom.parse(xml_file) - - data = { - 'total': percent(xmldoc.getElementsByTagName('coverage')[0].attributes['line-rate'].value), - 'fileReports': [], - } - - classes = xmldoc.getElementsByTagName('class') - for cls in classes: - file_report = { - 'filename': cls.attributes['filename'].value, - 'total': percent(cls.attributes['line-rate'].value), - 'coverage': {}, - } - lines = cls.getElementsByTagName('line') - for line in lines: - hits = int(line.attributes['hits'].value) - if hits >= 1: - # The API assumes 0 if a line is missing - file_report['coverage'][line.attributes['number'].value] = hits - data['fileReports'] += [file_report] - - # Try to send the data, raise an exception if we fail - r = requests.post(URL.format(token=token, commit=commit), data=data) - r.raise_for_status() - - -if __name__ == '__main__': - argc = len(sys.argv) - if argc < 3: - print("usage: codacycov.py TOKEN COMMIT [COVERAGE_XML]") - print("if COVERAGE_XML is not specified, default is coverage.xml") - elif argc < 4: - main(sys.argv[1], sys.argv[2]) - else: - main(sys.argv[1], sys.argv[2], sys.argv[3]) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index f229360..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requests diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..c34b498 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[bdist_wheel] +# This flag says that the code is written to work on both Python 2 and Python +# 3. If at all possible, it is good practice to do this. If you cannot, you +# will need to generate wheels for each Python version that you support. +universal=1 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7412de0 --- /dev/null +++ b/setup.py @@ -0,0 +1,58 @@ +# Always prefer setuptools over distutils +from setuptools import setup, find_packages +# To use a consistent encoding +from codecs import open +from os import path + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the relevant file +with open(path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = f.read() + +setup( + name='codacy-coverage', + + version='1.0.0', + + description='Codacy coverage reporter for Python', + long_description=long_description, + + url='https://github.com/codacy/python-codacy-coverage', + + author='Codacy', + author_email='team@codacy.com', + + license='MIT', + + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + 'Development Status :: 5 - Production/Stable', + + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Build Tools', + + 'License :: OSI Approved :: MIT License', + + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', + ], + + keywords='development coverage', + + packages=find_packages('src'), + package_dir={'': 'src'}, include_package_data=True, + + install_requires=['requests'], + + extras_require={ + 'dev': ['check-manifest'], + 'test': ['coverage'], + }, + + entry_points={ + 'console_scripts': [ + 'python-codacy-coverage=codacy:main', + ], + }, +) diff --git a/src/codacy/__init__.py b/src/codacy/__init__.py new file mode 100644 index 0000000..acf3dc3 --- /dev/null +++ b/src/codacy/__init__.py @@ -0,0 +1,5 @@ +import reporter + + +def main(): + return reporter.run() diff --git a/src/codacy/reporter.py b/src/codacy/reporter.py new file mode 100755 index 0000000..a3741af --- /dev/null +++ b/src/codacy/reporter.py @@ -0,0 +1,105 @@ +"""Codacy coverage reporter for Python""" + +import argparse +import json +import logging +import os +from xml.dom import minidom + +import requests + +logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s') + +CODACY_PROJECT_TOKEN = os.getenv('CODACY_PROJECT_TOKEN') +CODACY_BASE_API_URL = os.getenv('CODACY_BASE_API_URL', 'https://api.codacy.com') +URL = CODACY_BASE_API_URL + '/2.0/coverage/{commit}/python' +DEFAULT_REPORT_FILE = 'coverage.xml' + + +def get_git_revision_hash(): + import subprocess + + return subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip() + + +def parse_report_file(report_file): + # Convert decimal string to floored int percent value + percent = lambda s: int(float(s) * 100) + + # Parse the XML into the format expected by the API + report_xml = minidom.parse(report_file) + + report = { + 'language': "python", + 'total': percent(report_xml.getElementsByTagName('coverage')[0].attributes['line-rate'].value), + 'fileReports': [], + } + + classes = report_xml.getElementsByTagName('class') + for cls in classes: + file_report = { + 'filename': cls.attributes['filename'].value, + 'total': percent(cls.attributes['line-rate'].value), + 'coverage': {}, + } + lines = cls.getElementsByTagName('line') + for line in lines: + hits = int(line.attributes['hits'].value) + if hits >= 1: + # The API assumes 0 if a line is missing + file_report['coverage'][line.attributes['number'].value] = hits + report['fileReports'] += [file_report] + + return report + + +def upload_report(report, token, commit): + # Try to send the data, raise an exception if we fail + url = URL.format(commit=commit) + data = json.dumps(report) + headers = { + "project_token": token, + "Content-Type": "application/json" + } + + logging.debug(data) + + r = requests.post(url, data=data, headers=headers, allow_redirects=True) + + logging.debug(r.content) + r.raise_for_status() + + message = json.loads(r.content)['success'] + logging.info(message) + + +def main(report_file, token, commit): + """Parse XML file and POST it to the Codacy API""" + + logging.info("Parsing report file...") + report = parse_report_file(report_file) + + logging.info("Uploading report...") + upload_report(report, token, commit) + + +def run(): + parser = argparse.ArgumentParser(description='Codacy coverage reporter for Python.') + parser.add_argument("-r", "--report", type=str, help="coverage report file", default=DEFAULT_REPORT_FILE) + parser.add_argument("-c", "--commit", type=str, help="coverage report file") + parser.add_argument("-v", "--verbose", help="coverage report file", action="store_true") + + args = parser.parse_args() + + if args.verbose: + logging.Logger.setLevel(logging.getLogger(), logging.DEBUG) + + if not CODACY_PROJECT_TOKEN: + logging.error("environment variable CODACY_PROJECT_TOKEN is not defined.") + exit(1) + + if not args.commit: + args.commit = get_git_revision_hash() + + main(args.report, CODACY_PROJECT_TOKEN, args.commit)