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/Makefile b/Makefile
new file mode 100644
index 0000000..5ed5354
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,35 @@
+all: build test install
+
+build:
+ python setup.py build
+
+install: build
+ python setup.py develop
+
+test: pep8 pyflakes
+ sed 's?\$$1?'`pwd`'?' tests/filepath/cobertura.xml.tpl > tests/filepath/cobertura.xml
+ python setup.py test
+ rm tests/filepath/cobertura.xml || true
+
+test-all:
+ tox
+
+coverage:
+ rm coverage.xml || true
+ sed 's?\$$1?'`pwd`'?' tests/filepath/cobertura.xml.tpl > tests/filepath/cobertura.xml
+ coverage run --source src/codacy/ setup.py test
+ rm tests/filepath/cobertura.xml || true
+ coverage xml
+ python-codacy-coverage -r coverage.xml
+
+# requires "pip install pep8"
+pep8:
+ @git ls-files | grep \\.py$ | xargs pep8
+
+# requires "pip install pyflakes"
+pyflakes:
+ @export PYFLAKES_NODOCTEST=1 && \
+ git ls-files | grep \\.py$ | xargs pyflakes
+
+upload:
+ python setup.py sdist bdist_wheel upload
diff --git a/README.md b/README.md
deleted file mode 100644
index ccead75..0000000
--- a/README.md
+++ /dev/null
@@ -1,6 +0,0 @@
-# python-codacycov
-
-Submit Python `coverage` report to [Codacy](https://www.codacy.com/).
-
-DISCLAIMER: This is an unofficial project, and is not endorsed by or
-associated with Codacy in any way.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..fd88aa1
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,46 @@
+python-codacy-coverage
+======================
+
+Credits to Ryan for creating this! Python coverage reporter for Codacy https://www.codacy.com
+
+.. image:: https://api.codacy.com/project/badge/grade/3a8cf06a9db94d0ab3d55e0357bc8f9d
+ :target: https://www.codacy.com/app/Codacy/python-codacy-coverage
+ :alt: Codacy Badge
+.. image:: https://api.codacy.com/project/badge/coverage/3a8cf06a9db94d0ab3d55e0357bc8f9d
+ :target: https://www.codacy.com/app/Codacy/python-codacy-coverage
+ :alt: Codacy Badge
+.. image:: https://circleci.com/gh/codacy/python-codacy-coverage.png?style=shield&circle-token=:circle-token
+ :target: https://circleci.com/gh/codacy/python-codacy-coverage
+ :alt: Build Status
+.. image:: https://badge.fury.io/py/codacy-coverage.svg
+ :target: https://badge.fury.io/py/codacy-coverage
+ :alt: PyPI version
+
+Setup
+-----
+
+Codacy assumes that coverage is previously configured for your project.
+
+To generate the required coverage XML file, calculate coverage for your project as normal, by running:
+
+``coverage xml``
+
+Install codacy-coverage
+~~~~~~~~~~~~~~~~~~~~~~~
+
+You can install the coverage reporter by running:
+
+``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 -r coverage.xml``
diff --git a/circle.yml b/circle.yml
new file mode 100644
index 0000000..9ded75f
--- /dev/null
+++ b/circle.yml
@@ -0,0 +1,16 @@
+machine:
+ python:
+ version: 2.7.10
+
+dependencies:
+ post:
+ - sudo pip install pep8 --upgrade
+ - sudo pip install pyflakes
+ - sudo pip install coverage
+ - sudo pip install virtualenv==12.0.2
+
+test:
+ override:
+ - make test-all
+ - make install
+ - make coverage
\ No newline at end of file
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..9b303f5
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,8 @@
+[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
+
+[pep8]
+max_line_length = 120
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..cacfa58
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,69 @@
+# 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.rst'), encoding='utf-8') as f:
+ long_description = f.read()
+
+# Default version
+__version__ = '1.1.1'
+
+# Get the correct version from file
+try:
+ import version
+ __version__ = version.__version__
+except ImportError:
+ pass
+
+setup(
+ name='codacy-coverage',
+
+ version=__version__,
+
+ 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.7',
+ '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': ['nosetests', 'coverage'],
+ },
+
+ entry_points={
+ 'console_scripts': [
+ 'python-codacy-coverage=codacy:main',
+ ],
+ },
+ test_suite='tests'
+)
diff --git a/src/codacy/__init__.py b/src/codacy/__init__.py
new file mode 100644
index 0000000..6068f5c
--- /dev/null
+++ b/src/codacy/__init__.py
@@ -0,0 +1,6 @@
+from __future__ import absolute_import
+from . 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..8882e91
--- /dev/null
+++ b/src/codacy/reporter.py
@@ -0,0 +1,140 @@
+"""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_API_BASE_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']).decode("utf-8").strip()
+
+
+def get_git_directory():
+ import subprocess
+
+ return subprocess.check_output(['git', 'rev-parse', '--show-toplevel']).decode("utf-8").strip()
+
+
+def file_exists(rootdir, filename):
+ for root, subFolders, files in os.walk(rootdir):
+ if filename in files:
+ return True
+ else:
+ for subFolder in subFolders:
+ return file_exists(subFolder, filename)
+ return False
+
+
+def generate_filename(sources, filename):
+ def strip_prefix(line, prefix):
+ if line.startswith(prefix):
+ return line[len(prefix):]
+ else:
+ return line
+
+ git_directory = get_git_directory()
+
+ for source in sources:
+ if file_exists(source, filename):
+ return strip_prefix(source, git_directory).strip("/") + "/" + filename.strip("/")
+
+ return filename
+
+
+def parse_report_file(report_file):
+ """Parse XML file and POST it to the Codacy API
+ :param report_file:
+ """
+
+ # Convert decimal string to floored int percent value
+ def percent(s):
+ return 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': [],
+ }
+
+ sources = [x.firstChild.nodeValue for x in report_xml.getElementsByTagName('source')]
+ classes = report_xml.getElementsByTagName('class')
+ for cls in classes:
+ file_report = {
+ 'filename': generate_filename(sources, 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.text)['success']
+ logging.info(message)
+
+
+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="git commit hash")
+ parser.add_argument("-v", "--verbose", help="show debug information", 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()
+
+ if not os.path.isfile(args.report):
+ logging.error("Coverage report " + args.report + " not found.")
+ exit(1)
+
+ logging.info("Parsing report file...")
+ report = parse_report_file(args.report)
+
+ logging.info("Uploading report...")
+ upload_report(report, CODACY_PROJECT_TOKEN, args.commit)
diff --git a/tests.py b/tests.py
new file mode 100644
index 0000000..a1f14c7
--- /dev/null
+++ b/tests.py
@@ -0,0 +1,36 @@
+import unittest
+import codacy.reporter
+import json
+
+
+class ReporterTests(unittest.TestCase):
+ def compare_parse_result(self, generated_filename, expected_filename):
+ def file_get_contents(filename):
+ with open(filename) as f:
+ return f.read()
+
+ generated = codacy.reporter.parse_report_file(generated_filename)
+
+ json_content = file_get_contents(expected_filename)
+ expected = json.loads(json_content)
+
+ self.assertEqual(generated, expected)
+
+ def test_parser_coverage3(self):
+ self.maxDiff = None
+
+ self.compare_parse_result('tests/coverage3/cobertura.xml', 'tests/coverage3/coverage.json')
+
+ def test_parser_coverage4(self):
+ self.maxDiff = None
+
+ self.compare_parse_result('tests/coverage4/cobertura.xml', 'tests/coverage4/coverage.json')
+
+ def test_parser_git_filepath(self):
+ self.maxDiff = None
+
+ self.compare_parse_result('tests/filepath/cobertura.xml', 'tests/filepath/coverage.json')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/coverage3/cobertura.xml b/tests/coverage3/cobertura.xml
new file mode 100644
index 0000000..015adc2
--- /dev/null
+++ b/tests/coverage3/cobertura.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/coverage3/coverage.json b/tests/coverage3/coverage.json
new file mode 100644
index 0000000..b94ec68
--- /dev/null
+++ b/tests/coverage3/coverage.json
@@ -0,0 +1,32 @@
+{
+ "total": 87,
+ "fileReports": [
+ {
+ "total": 87,
+ "coverage": {
+ "5": 1,
+ "4": 1,
+ "6": 2
+ },
+ "filename": "src/test/resources/TestSourceFile.scala"
+ },
+ {
+ "total": 87,
+ "coverage": {
+ "9": 1,
+ "10": 1
+ },
+ "filename": "src/test/resources/TestSourceFile.scala"
+ },
+ {
+ "total": 87,
+ "coverage": {
+ "1": 1,
+ "3": 1,
+ "2": 1
+ },
+ "filename": "src/test/resources/TestSourceFile2.scala"
+ }
+ ],
+ "language": "python"
+}
diff --git a/tests/coverage4/cobertura.xml b/tests/coverage4/cobertura.xml
new file mode 100644
index 0000000..0e71e5d
--- /dev/null
+++ b/tests/coverage4/cobertura.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+ /Users/rafaelcortes/Documents/qamine/python-codacycov
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/coverage4/coverage.json b/tests/coverage4/coverage.json
new file mode 100644
index 0000000..e0a780b
--- /dev/null
+++ b/tests/coverage4/coverage.json
@@ -0,0 +1,49 @@
+{
+ "total": 50.0,
+ "fileReports": [
+ {
+ "total": 66.67,
+ "coverage": {
+ "1": 1,
+ "4": 1
+ },
+ "filename": "src/codacy/__init__.py"
+ },
+ {
+ "total": 49.15,
+ "coverage": {
+ "50": 1,
+ "60": 1,
+ "80": 1,
+ "52": 1,
+ "26": 1,
+ "20": 1,
+ "49": 1,
+ "44": 1,
+ "42": 1,
+ "43": 1,
+ "3": 1,
+ "5": 1,
+ "4": 1,
+ "7": 1,
+ "6": 1,
+ "9": 1,
+ "11": 1,
+ "15": 1,
+ "14": 1,
+ "17": 1,
+ "16": 1,
+ "55": 1,
+ "54": 1,
+ "31": 1,
+ "30": 1,
+ "51": 1,
+ "36": 1,
+ "34": 1,
+ "57": 1
+ },
+ "filename": "src/codacy/reporter.py"
+ }
+ ],
+ "language": "python"
+}
\ No newline at end of file
diff --git a/tests/filepath/cobertura.xml.tpl b/tests/filepath/cobertura.xml.tpl
new file mode 100644
index 0000000..895abb8
--- /dev/null
+++ b/tests/filepath/cobertura.xml.tpl
@@ -0,0 +1,87 @@
+
+
+
+
+
+ $1/src/codacy
+ $1/src/codacy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/filepath/coverage.json b/tests/filepath/coverage.json
new file mode 100644
index 0000000..e0a780b
--- /dev/null
+++ b/tests/filepath/coverage.json
@@ -0,0 +1,49 @@
+{
+ "total": 50.0,
+ "fileReports": [
+ {
+ "total": 66.67,
+ "coverage": {
+ "1": 1,
+ "4": 1
+ },
+ "filename": "src/codacy/__init__.py"
+ },
+ {
+ "total": 49.15,
+ "coverage": {
+ "50": 1,
+ "60": 1,
+ "80": 1,
+ "52": 1,
+ "26": 1,
+ "20": 1,
+ "49": 1,
+ "44": 1,
+ "42": 1,
+ "43": 1,
+ "3": 1,
+ "5": 1,
+ "4": 1,
+ "7": 1,
+ "6": 1,
+ "9": 1,
+ "11": 1,
+ "15": 1,
+ "14": 1,
+ "17": 1,
+ "16": 1,
+ "55": 1,
+ "54": 1,
+ "31": 1,
+ "30": 1,
+ "51": 1,
+ "36": 1,
+ "34": 1,
+ "57": 1
+ },
+ "filename": "src/codacy/reporter.py"
+ }
+ ],
+ "language": "python"
+}
\ No newline at end of file
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..7149960
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,6 @@
+[tox]
+envlist = py27, py350
+
+[testenv]
+commands = make test
+whitelist_externals=make
diff --git a/version.py b/version.py
new file mode 100644
index 0000000..b3ddbc4
--- /dev/null
+++ b/version.py
@@ -0,0 +1 @@
+__version__ = '1.1.1'