Skip to content

Commit

Permalink
thanks nows read data from the packages and json-api (even tho the da…
Browse files Browse the repository at this point in the history
…ta is missing) (#23)

* Added ability to read local metadata and search for funding links

* Fixes running tests via tox

Corrected linting errors and removed rouge 'print' calls.

Moved to using nosetest as the setup.py test runner. This is mainly as nose
give clearer error messages when something goes wrong.

Also of note there were pinned dependencies which have been changed to
'at least' version dependencies. For reasons why see
https://caremad.io/posts/2013/07/setup-vs-requirement/

Laster, we currently have a README.md and a README.rst. I'm unsure which one of these
we mean to use. Shouldn't we at the very least keep all of the following in
the same markup format: README, HISTORY, CONTRIBUTING, AUTHORS,

* Attempts to make clever code more readable
  • Loading branch information
tomdottom authored and phildini committed May 29, 2018
1 parent 1737fe0 commit b26aadd
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 48 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ include CONTRIBUTING.rst
include HISTORY.rst
include LICENSE
include README.rst
include README.md

recursive-include tests *
recursive-exclude * __pycache__
Expand Down
1 change: 1 addition & 0 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ tox==2.9.1
coverage==4.4.2
Sphinx==1.6.5
twine==1.9.1
ddt
16 changes: 9 additions & 7 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@
history = history_file.read()

requirements = [
'Click>=6.0',
'humanfriendly==4.8',
'requirements-parser==0.2.0',
'termcolor==1.1.0',
'Click>=6',
'humanfriendly>=4',
'requirements-parser>=0.2',
'termcolor>=1',
'setuptools>=',
'requests>=2',
]

setup_requirements = [
# TODO(phildini): Put setup requirements (distutils extensions, etc.) here
'nose'
]

test_requirements = [
# TODO: Put package test requirements here
'ddt',
]

setup(
Expand Down Expand Up @@ -60,7 +62,7 @@
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
],
test_suite='tests',
test_suite='nose.collector',
tests_require=test_requirements,
setup_requires=setup_requirements,
)
Empty file.
1 change: 1 addition & 0 deletions tests/fixtures/mosw-0.1.dist-info/METADATA
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Summary: An example package for testing
Home-page: http://ministry-of-silly-walks.python
Author: Tom Marks
Author-email: [email protected]
Project-URL: Funding, http://ministry-of-silly-walks.python/fundme
License: UNKNOWN
Platform: UNKNOWN
Classifier: Programming Language :: Python
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/mosw-0.1.dist-info/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
},
"generator": "bdist_wheel (0.30.0.a0)",
"metadata_version": "2.0",
"name": "click",
"name": "mosw",
"summary": "An example package for testing",
"version": "0.1"
}
101 changes: 101 additions & 0 deletions tests/test_package_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,97 @@

"""Test for the package tools for extracting funding metadata"""

from collections import namedtuple
import os
import sys
from textwrap import dedent
import unittest

from ddt import ddt, data, unpack
import pkg_resources

import thanks.package_tools as ptools

# Add the mock packages in test/fixtures to sys.path
package_fixture_paths = os.path.join(os.path.dirname(
os.path.realpath(__file__)), 'fixtures')
sys.path.append(package_fixture_paths)

DistTestCase = namedtuple(
'DistTestCase',
['description', 'dist', 'expected_path'],
)

DistMetadataPathTestCases = (
DistTestCase(
description='''
Create a short egg-info name.
''',
dist=pkg_resources.EggInfoDistribution(
location= '/path/to/dist',
project_name='crunchy-frog',
version="0.1",
py_version=None,
platform=None,
),
expected_path='/path/to/dist/crunchy_frog-0.1.egg-info/PKG-INFO'
),
DistTestCase(
description='''
Ignores platform with no py_version
''',
dist=pkg_resources.EggInfoDistribution(
location= '/path/to/dist',
project_name='crunchy-frog',
version="0.1",
py_version=None,
platform='foo',
),
expected_path='/path/to/dist/crunchy_frog-0.1.egg-info/PKG-INFO'
),
DistTestCase(
description='''
Includes py version
''',
dist=pkg_resources.EggInfoDistribution(
location= '/path/to/dist',
project_name='crunchy-frog',
version="0.1",
py_version='3.6',
platform=None,
),
expected_path='/path/to/dist/crunchy_frog-0.1-py3.6.egg-info/PKG-INFO'
),
DistTestCase(
description='''
Create a short egg name.
''',
dist=pkg_resources.Distribution(
location= '/path/to/dist',
project_name='crunchy-frog',
version="0.1",
py_version=None,
platform=None,
),
expected_path='/path/to/dist/crunchy_frog-0.1.egg/EGG-INFO/PKG-INFO'
),
DistTestCase(
description='''
Create a short dist info name
''',
dist=pkg_resources.DistInfoDistribution(
location= '/path/to/dist',
project_name='crunchy-frog',
version="0.1",
py_version=None,
platform=None,
),
expected_path='/path/to/dist/crunchy_frog-0.1.dist-info/METADATA'
),
)


@ddt
class TestPackageTools(unittest.TestCase):

def test_get_local_funding_metadata_from_name(self):
Expand All @@ -26,6 +105,28 @@ def test_get_local_funding_metadata_from_name(self):
'http://ministry-of-silly-walks.python/fundme'
)

@data(*DistMetadataPathTestCases)
def test_generate_egg_project_location(self, tc):
path = ptools.get_local_dist_metadata_filepath(tc.dist)

self.assertEqual(path, tc.expected_path)

def test_metadata_parsing(self):
metadata_file = dedent("""
Author: The Black Night
Maintainer: King Authur
Project-URL: Funding, https://monty.python/rabbits
""")
expected_metadata = {
"author": "The Black Night",
"maintainer": "King Authur",
"funding_url": "https://monty.python/rabbits"
}

metadata = ptools.parse_metadata(metadata_file)

self.assertEqual(metadata, expected_metadata)


if __name__ == '__main__':
unittest.main()
97 changes: 84 additions & 13 deletions thanks/package_tools.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import json
from functools import reduce
from itertools import chain, takewhile
import os
import pkg_resources
import re


class MetaDataNotFound(Exception):
Expand All @@ -15,31 +18,99 @@ def get_local_dist(package_name):


def get_dist_metadata(dist):
metadata_path = '{}/{}-{}.dist-info/{}'.format(
dist.location,
dist.project_name,
dist.parsed_version.public,
'metadata.json',
)
metadata_path = get_local_dist_metadata_filepath(dist)
with open(metadata_path) as fh:
metadata = json.load(fh)
metadata = parse_metadata(fh.read())
return metadata


def get_funding_data(metadata):
return (
metadata['extensions']['python.details']['project_urls']['Funding']
)
return metadata.get('funding_url')


def get_local_funding_metadata(package_name):
def get_local_dist_metadata_filepath(dist):
# Dist filename syntax
# name ["-" version ["-py" pyver ["-" required_platform]]] "." ext
# https://setuptools.readthedocs.io/en/latest/formats.html#filename-embedded-metadata

def valid_component(component):
return component[1]

# Stop taking filename components at the first missing/invalid component
filename_component = takewhile(valid_component, (
('', pkg_resources.to_filename(pkg_resources.safe_name(dist.project_name))),
('-', pkg_resources.to_filename(pkg_resources.safe_version(dist.version))),
('-py', dist.py_version),
('-', dist.platform),
))
filename = ''.join(chain(*filename_component))

if isinstance(dist, pkg_resources.EggInfoDistribution):
ext = 'egg-info'
metadata_file = 'PKG-INFO'
elif isinstance(dist, pkg_resources.DistInfoDistribution):
ext = 'dist-info'
metadata_file = 'METADATA'
elif isinstance(dist, pkg_resources.Distribution):
ext = os.path.join('egg', 'EGG-INFO')
metadata_file = 'PKG-INFO'
else:
ext = None
metadata_file = None

filename = '{}.{}'.format(filename, ext)
path = os.path.join(dist.location, filename, metadata_file)

if ext:
return path
else:
return None


metadata_patterns = re.compile(r"""
(\s*Author:\s+(?P<author>.*)\s*)? # Author
(\s*Maintainer:\s+(?P<maintainer>.+)\s*)? # Maintainer
(\s*Project-URL:\sFunding,\s+(?P<funding_url>.+)\s*)? # Funding URL
""", re.VERBOSE)


def get_line_metadata(line):
return metadata_patterns.search(line).groupdict()


def filter_empty_metadata(metadata):
return dict((k, v) for k, v in metadata.items() if v)


def parse_metadata(metadata):
metadata = (
filter_empty_metadata(get_line_metadata(line))
for line in metadata.splitlines()
)
metadata = [m for m in metadata if m]
metadata = reduce(
lambda x, y: dict((k, v) for k, v in chain(x.items(), y.items())),
metadata,
{},
)
return metadata


def get_local_metadata(package_name):
try:
dist = get_local_dist(package_name)
metadata = get_dist_metadata(dist)
funding_url = get_funding_data(metadata)
except FileNotFoundError:
# No metadata.json file locally
raise MetaDataNotFound()

return metadata


def get_local_funding_metadata(package_name):
try:
metadata = get_local_metadata(package_name)
funding_url = get_funding_data(metadata)
except KeyError:
# Package not available locally,
# or there isn't a 'Funding' entry in the project_urls
Expand Down
12 changes: 0 additions & 12 deletions thanks/thanks.json

This file was deleted.

34 changes: 20 additions & 14 deletions thanks/thanks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import requests
from termcolor import colored, cprint

from . import package_tools


JSON_FILE = ("{}/thanks.json".format(os.path.dirname(os.path.realpath(__file__))))

Expand All @@ -17,18 +19,8 @@
class Thanks():
def __init__(self, debug=False):
self.debug = debug
self.data = self.load_local_project_data()
self.give_thanks_to = {}

def load_local_project_data(self):
print('Loading data about {}'.format(colored('contributors...', 'cyan')))
with open(JSON_FILE, 'r') as fh:
data = json.load(fh)
for project_name in data.keys():
authors = ', '.join(data[project_name].get('authors', []))
data[project_name]['authors'] = authors
return data

def find_project_details(self, requirements_list):
print('Scanning your {} file...'.format(colored('requirements', 'red')))
reqs = [
Expand Down Expand Up @@ -61,15 +53,29 @@ def _display_thanks(self):
horizontal_bar=' ',
vertical_bar=' ',
))
cprint(
''.join([
"If you see projects with ",
colored("MISSING FUNDING INFORMATION ", "red"),
"then why not submit a pull request ",
"the project repository asking the author to ",
colored("ADD A 'FUNDING' PROJECT_URL ", "yellow"),
"to the projects setup.py"
]),
attrs=['bold']
)

def get_local_data(self, project_name):
if project_name in self.data:
try:
metadata = package_tools.get_local_metadata(project_name)
data = ProjectData(
name=project_name,
funding_link=self.data[project_name]['url'],
authors=self.data[project_name]['authors']
funding_link=metadata['funding_url'],
authors=metadata['author']
)
else:
except KeyError:
data = None
except package_tools.MetaDataNotFound:
data = None
return data

Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ commands = flake8 thanks
setenv =
PYTHONPATH = {toxinidir}

commands = python setup.py test
commands = python setup.py nosetests

0 comments on commit b26aadd

Please sign in to comment.