Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.1.1
current_version = 0.2.0
commit = False
tag = False

Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include reader/*.cfg
16 changes: 13 additions & 3 deletions reader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@

See https://github.com/realpython/reader/ for more information
"""
import importlib_resources as _resources
try:
from configparser import ConfigParser as _ConfigParser
except ImportError: # Python 2
from ConfigParser import ConfigParser as _ConfigParser


# Version of realpython-reader package
__version__ = "0.1.1"
__version__ = "0.2.0"

# URL of Real Python feed
URL = "https://realpython.com/atom.xml"
# Read URL of feed from config file
_cfg = _ConfigParser()
with _resources.path("reader", "config.cfg") as _path:
_cfg.read(str(_path))
URL = _cfg.get("feed", "url")
12 changes: 8 additions & 4 deletions reader/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@
Version:
--------

- realpython-reader v0.1.1
- realpython-reader v0.2.0
"""
# Standard library imports
import sys

# Reader imports
import reader
from reader import feed
from reader import viewer

Expand All @@ -63,16 +64,19 @@ def main(): # type: () -> None
# Should links be shown in the text
show_links = "-l" in opts or "--show-links" in opts

# Get URL from config file
url = reader.URL

# An article ID is given, show article
if args:
for article_id in args:
article = feed.get_article(article_id, show_links)
article = feed.get_article(article_id, links=show_links, url=url)
viewer.show(article)

# No ID is given, show list of articles
else:
site = feed.get_site()
titles = feed.get_titles()
site = feed.get_site(url=url)
titles = feed.get_titles(url=url)
viewer.show_list(site, titles)


Expand Down
2 changes: 2 additions & 0 deletions reader/config.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[feed]
url = https://realpython.com/atom.xml
23 changes: 12 additions & 11 deletions reader/feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,23 @@
_CACHED_FEEDS = dict() # type: Dict[str, feedparser.FeedParserDict]


def _feed(): # type: () -> feedparser.FeedParserDict
def _feed(url=URL): # type: (str) -> feedparser.FeedParserDict
"""Cache contents of the feed, so it's only read once"""
if URL not in _CACHED_FEEDS:
_CACHED_FEEDS[URL] = feedparser.parse(URL)
return _CACHED_FEEDS[URL]
if url not in _CACHED_FEEDS:
_CACHED_FEEDS[url] = feedparser.parse(url)
return _CACHED_FEEDS[url]


def get_site(): # type: () -> str
def get_site(url=URL): # type: (str) -> str
"""Get name and link to web site of the feed"""
info = _feed().feed
return "{info.title} ({info.link})".format(info=info)
info = _feed(url).feed
return u"{info.title} ({info.link})".format(info=info)


def get_article(article_id, links=False): # type: (str, bool) -> str
def get_article(article_id, links=False, url=URL):
# type: (str, bool, str) -> str
"""Get article from feed with the given ID"""
articles = _feed().entries
articles = _feed(url).entries
try:
article = articles[int(article_id)]
except (IndexError, ValueError):
Expand All @@ -49,7 +50,7 @@ def get_article(article_id, links=False): # type: (str, bool) -> str
return u"# {}\n\n{}".format(article.title, text)


def get_titles(): # type: () -> List[str]
def get_titles(url=URL): # type: (str) -> List[str]
"""List titles in feed"""
articles = _feed().entries
articles = _feed(url).entries
return [a.title for a in articles]
4 changes: 2 additions & 2 deletions reader/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ def show(article): # type: (str) -> None

def show_list(site, titles): # type: (str, List[str]) -> None
"""Show list of articles"""
print("The latest tutorials from {}".format(site))
print(u"The latest tutorials from {}".format(site))
for article_id, title in enumerate(titles):
print("{:>3} {}".format(article_id, title))
print(u"{:>3} {}".format(article_id, title))
8 changes: 6 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# This call to setup() does all the work
setup(
name="realpython-reader",
version="0.1.1",
version="0.2.0",
description="Read Real Python tutorials",
long_description=README,
long_description_content_type="text/markdown",
Expand All @@ -28,6 +28,10 @@
"Programming Language :: Python :: 3",
],
packages=["reader"],
install_requires=["feedparser", "html2text", "typing"],
package_data={"reader": ["reader/config.cfg"]},
include_package_data=True,
install_requires=[
"feedparser", "html2text", "importlib_resources", "typing"
],
entry_points={"console_scripts": ["realpython=reader.__main__:main"]},
)
38 changes: 17 additions & 21 deletions tests/test_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,77 +13,73 @@


@pytest.fixture
def monkeypatch_feed(monkeypatch):
def local_feed():
"""Use local file instead of downloading feed from web"""
local_path = os.path.join(HERE, "realpython_20180919.xml")
monkeypatch.setattr(feed, "URL", local_path)
return local_path
return os.path.join(HERE, "realpython_20180919.xml")


@pytest.fixture
def monkeypatch_summary_feed(monkeypatch):
def local_summary_feed():
"""Use local file instead of downloading feed from web"""
local_path = os.path.join(HERE, "realpython_descriptions_20180919.xml")
monkeypatch.setattr(feed, "URL", local_path)
return local_path
return os.path.join(HERE, "realpython_descriptions_20180919.xml")


#
# Tests
#
def test_site(monkeypatch_feed):
def test_site(local_feed):
"""Test that we can read the site title and link"""
expected = "Real Python (https://realpython.com/)"
assert feed.get_site() == expected
assert feed.get_site(url=local_feed) == expected


def test_article_title(monkeypatch_feed):
def test_article_title(local_feed):
"""Test that title is added at top of article"""
article_id = 0
title = feed.get_titles()[article_id]
article = feed.get_article(article_id)
title = feed.get_titles(url=local_feed)[article_id]
article = feed.get_article(article_id, url=local_feed)

assert article.strip("# ").startswith(title)


def test_article(monkeypatch_feed):
def test_article(local_feed):
"""Test that article is returned"""
article_id = 2
article_phrases = [
"logging.info('This is an info message')",
"By using the `level` parameter",
" * `level`: The root logger",
]
article = feed.get_article(article_id)
article = feed.get_article(article_id, url=local_feed)

for phrase in article_phrases:
assert phrase in article


def test_titles(monkeypatch_feed):
def test_titles(local_feed):
"""Test that titles are found"""
titles = feed.get_titles()
titles = feed.get_titles(url=local_feed)

assert len(titles) == 20
assert titles[0] == "Absolute vs Relative Imports in Python"
assert titles[9] == "Primer on Python Decorators"


def test_summary(monkeypatch_summary_feed):
def test_summary(local_summary_feed):
"""Test that summary feeds can be read"""
article_id = 1
summary_phrases = [
"Get the inside scoop",
"this list of\ninformative videos",
]
summary = feed.get_article(article_id)
summary = feed.get_article(article_id, url=local_summary_feed)

for phrase in summary_phrases:
assert phrase in summary


def test_invalid_article_id(monkeypatch_feed):
def test_invalid_article_id(local_feed):
"""Test that invalid article ids are handled gracefully"""
article_id = "wrong"
with pytest.raises(SystemExit):
feed.get_article(article_id)
feed.get_article(article_id, url=local_feed)