pax_global_header00006660000000000000000000000064130656275560014531gustar00rootroot0000000000000052 comment=6998d4073507eea228185e02ad1d9071c77fa955 python-xkcd-2.4.2/000077500000000000000000000000001306562755600140065ustar00rootroot00000000000000python-xkcd-2.4.2/.gitignore000066400000000000000000000010531306562755600157750ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ bin/ build/ develop-eggs/ dist/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # Rope .ropeproject # Django stuff: *.log *.pot # Sphinx documentation docs/_build/ docs/build/ python-xkcd-2.4.2/.travis.yml000066400000000000000000000001551306562755600161200ustar00rootroot00000000000000language: python python: - "2.6" - "2.7" - "3.3" - "3.4" - "3.5" script: - python setup.py test python-xkcd-2.4.2/LICENSE000066400000000000000000000020721306562755600150140ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2012-2017 Ben Rosser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-xkcd-2.4.2/MANIFEST.in000066400000000000000000000000421306562755600155400ustar00rootroot00000000000000include README.md include LICENSE python-xkcd-2.4.2/README.md000066400000000000000000000057001306562755600152670ustar00rootroot00000000000000# xkcd v2.4.2 [![Build Status](https://travis-ci.org/TC01/python-xkcd.svg?branch=master)](https://travis-ci.org/TC01/python-xkcd) ### A Python interface to xkcd.com By Ben Rosser, released under MIT License (see LICENSE for full text). This is a Python library for accessing and retrieving links to comics from the xkcd webcomic by Randall Munroe. It is NOT endorsed or made by him, it's an entirely independent project. It makes use of the JSON interface to Randall's site to retrieve comic data. Both Python 2 and Python 3 are supported, and there are no dependencies beyond the Python standard library, so xkcd's footprint should be very light. There is support for accessing specific comics, the latest comic, or a random comic. Comic metadata can be queried and the comics themselves can be downloaded onto your local system. The goal is simply to provide a relatively Pythonic wrapper around the xkcd API for any Python program or library that wants to access information about xkcd comics, for one reason or another. The xkcd module, as of version 2.4.0, also supports getting information on What If articles from whatif.xkcd.com. This information is generated by scraping the What If archive page with a HTML parser. Full API documentation is available [here](https://pythonhosted.org/xkcd/). ## Changelog: ### Version 2.4.2: * Switched to using HTTPS URLs for all xkcd queries. ### Version 2.4.1: * Routines that take comic/article numbers (e.g. xkcd.getComic()) now also can take strings containing cardinal numbers. ### Version 2.4.0: * Added preliminary What If support; routines for querying basic data about What If articles now exist. * Comic.download() will create its default directory (~/Downloads) if it does not already exist, rather than simply failing. * All prints to standard output are now wrapped in "silent" options that now default to True (this affects xkcd.getComic and Comic.download); if silent is set, output won't be printed. * Significantly improved documentation for all available functions and classes. ### Version 2.3.3: * Made pypandoc conversion optional; long_description will be MD formatted if it cannot be imported (and rST-formatted if it can). ### Version 2.3.2: * Fixed distutils URL to point at TC01/python-xkcd, not TC01/xkcd. * Started using pypandoc to dynamically turn README.md into a RST long-description. ### Version 2.3: * Fixed ASCII bug in Python 2.x * Created Sphinx documentation and uploaded it to pythonhosted.org ### Version 2.2: * Fixed very silly bug with xkcd.getComic() * Added a getExplanation() which returns an explainxkcd link for a Comic(). * Added support for Python 3! ### Version 2.1: * Fixed bugs with Comic.download() function * Added optional parameter to Comic.download() to change name of output file * Added more information to long_description text ## Credits: * Ben Rosser : Developer Contributions from (github users, unless indicated otherwise): * @Kyu * Tanya Sandoval (@tsando) python-xkcd-2.4.2/docs/000077500000000000000000000000001306562755600147365ustar00rootroot00000000000000python-xkcd-2.4.2/docs/Makefile000066400000000000000000000126751306562755600164110ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/xkcd.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/xkcd.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/xkcd" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/xkcd" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." python-xkcd-2.4.2/docs/source/000077500000000000000000000000001306562755600162365ustar00rootroot00000000000000python-xkcd-2.4.2/docs/source/conf.py000066400000000000000000000172061306562755600175430ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # xkcd documentation build configuration file, created by # sphinx-quickstart on Mon Jul 21 17:08:09 2014. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # 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('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'xkcd' copyright = u'2014-2016, Ben Rosser' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '2.4.2' # The full version, including alpha/beta/rc tags. release = '2.4.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'xkcddoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The 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', 'xkcd.tex', u'xkcd Documentation', u'Ben Rosser', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'xkcd', u'xkcd Documentation', [u'Ben Rosser'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'xkcd', u'xkcd Documentation', u'Ben Rosser', 'xkcd', 'An unofficial python interface to xkcd', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # Stick this on the path. sys.path.append(os.path.join(os.getcwd(), "../../")) python-xkcd-2.4.2/docs/source/index.rst000066400000000000000000000007341306562755600201030ustar00rootroot00000000000000.. xkcd documentation master file, created by sphinx-quickstart on Mon Jul 21 17:08:09 2014. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to the documentation for python-xkcd! ============================================= Contents: .. toctree:: :maxdepth: 2 .. automodule:: xkcd :members: Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` python-xkcd-2.4.2/setup.cfg000066400000000000000000000001751306562755600156320ustar00rootroot00000000000000[build_sphinx] source-dir = docs/source build-dir = docs/build all_files = 1 [upload_sphinx] upload-dir = docs/build/html python-xkcd-2.4.2/setup.py000066400000000000000000000073741306562755600155330ustar00rootroot00000000000000# Based from http://pythonhosted.org/setuptools/setuptools.html#automatic-script-creation from setuptools import setup, find_packages # Always prefer setuptools over distutils from codecs import open # To use a consistent encoding from os import path # We shouldn't need pypandoc on remote users's systems just because I dislike rST. try: # Depend on pypandoc for turning markdown readme into RST because # PyPI doesn't yet support this. import pypandoc here = path.abspath(path.dirname(__file__)) long_description = pypandoc.convert("README.md", "rst") except ImportError: 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='xkcd', # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # http://packaging.python.org/en/latest/tutorial.html#version version='2.4.2', description="Library to access xkcd.com", long_description=long_description, # The project's main homepage. url='https://github.com/TC01/python-xkcd', # Author details author = "Ben Rosser", author_email='rosser.bjr@gmail.com', # Choose your license license='MIT', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ # How mature is this project? Common values are # 3 - Alpha # 4 - Beta # 5 - Production/Stable 'Development Status :: 5 - Production/Stable', # Indicate who your project is intended for 'Intended Audience :: Developers', 'Intended Audience :: End Users/Desktop', 'Topic :: Software Development :: Libraries', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', # Pick your license as you wish (should match "license" above) 'License :: OSI Approved :: MIT License', # Specify the Python versions you support here. In particular, ensure # that you indicate whether you support Python 2, Python 3 or both. 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', ], # What does your project relate to? keywords='xkcd webcomic whatif', # You can just specify the packages manually here if your project is # simple. Or you can use find_packages(). # packages=find_packages(exclude=['contrib', 'docs', 'tests*']), py_modules=['xkcd'], # List run-time dependencies here. These will be installed by pip when your # project is installed. For an analysis of "install_requires" vs pip's # requirements files see: # https://packaging.python.org/en/latest/technical.html#install-requires-vs-requirements-files #install_requires=['peppercorn'], # If there are data files included in your packages that need to be # installed, specify them here. If using Python 2.6 or less, then these # have to be included in MANIFEST.in as well. #package_data={ # 'sample': ['package_data.dat'], #}, # Although 'package_data' is the preferred approach, in some case you may # need to place data files outside of your packages. # see http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # In this case, 'data_file' will be installed into '/my_data' #data_files=[('my_data', ['data/data_file'])], # To provide executable scripts, use entry points in preference to the # "scripts" keyword. Entry points provide cross-platform support and allow # pip to create the appropriate form of executable for the target platform. # entry_points={ # 'console_scripts': [ # 'calcpkg=calcrepo.calcpkg:main', # ], # }, # Test suites. test_suite = 'tests', ) python-xkcd-2.4.2/tests/000077500000000000000000000000001306562755600151505ustar00rootroot00000000000000python-xkcd-2.4.2/tests/__init__.py000066400000000000000000000000001306562755600172470ustar00rootroot00000000000000python-xkcd-2.4.2/tests/xkcd_test.py000077500000000000000000000017151306562755600175210ustar00rootroot00000000000000# unit test suite for python-xkcd import os import unittest import xkcd class TestXkcd(unittest.TestCase): def test_no_such_comic(self): bad = xkcd.getComic(-100) self.assertEqual(bad.number, -1) def test_comic(self): # Get a comic to test. test = xkcd.getComic(869) self.assertEqual(test.number, 869) self.assertEqual(test.title, "Server Attention Span") self.assertEqual(test.imageName, "server_attention_span.png") def test_download_comic(self): # Try to download a comic. dlname = "xkcd-unittestserver_attention_span.png" test = xkcd.getComic(869) test.download(outputFile=dlname) path = os.path.join(os.path.expanduser("~"), "Downloads", dlname) self.assertTrue(os.path.exists(path)) # Delete the downloaded file os.remove(path) def test_whatif(self): # Get a What If to test. test = xkcd.getWhatIf(3) self.assertEqual(test.number, 3) self.assertEqual(test.title, "Yoda") if __name__ == '__main__': unittest.main() python-xkcd-2.4.2/xkcd.py000066400000000000000000000414741306562755600153230ustar00rootroot00000000000000"""Python library for accessing xkcd.com. This is a Python library for accessing and retrieving links to comics from the xkcd webcomic by Randall Munroe. It is NOT endorsed or made by him, it's an entirely independent project. It makes use of the JSON interface to Randall's site to retrieve comic data. Both Python 2 and Python 3 are supported, and there are no dependencies beyond the Python standard library, so xkcd's footprint should be very light. There is support for accessing specific comics, the latest comic, or a random comic. Comic metadata can be queried and the comics themselves can be downloaded onto your local system. The goal is simply to provide a relatively Pythonic wrapper around the xkcd API for any Python program or library that wants to access information about xkcd comics, for one reason or another. The xkcd module, as of version 2.4.0, also supports getting information on What If articles from whatif.xkcd.com. This information is generated by scraping the What If archive page with a HTML parser.""" import copy import json import os import random import sys import webbrowser # Python 3 support! if sys.version_info[0] <= 2: import urllib2 as urllib import HTMLParser else: # This is kind of broken but I'm not sure of a better way. import urllib.request as urllib import html.parser as HTMLParser # Define the URLs as globals. xkcdUrl = "https://www.xkcd.com/" # The URL for xkcd. imageUrl = "https://imgs.xkcd.com/comics/" # The root URL for image retrieval. explanationUrl = "https://explainxkcd.com/" # The URL of the explanation. archiveUrl = "https://what-if.xkcd.com/archive/" # The What If Archive URL. class WhatIf: """ Class representing an xkcd What If article. The WhatIf class is somewhat simpler than the :class:`Comic` class. It simply provides functions for querying information about the link to, title of, and index of a What If article. Unlike the :class:`Comic` class, you are not meant to construct them directly. Instead, call :func:`getWhatIfArchive` to produce a dictionary mapping numbers to WhatIf objects and then select the one(s) you are interested in. """ def __init__(self): self.number = -1 self.title = '' self.link = '' def __str__(self): return "What If object for " + self.link def __repr__(self): return self.__str__() def getTitle(self): """Returns the title of the What If article.""" return self.title def getNumber(self): """Returns the number of the What If article.""" return self.number def getLink(self): """Returns a link to the What If article.""" return self.link # Possibly, BeautifulSoup or MechanicalSoup or something would be nicer # But xkcd currently has no external dependencies and I'd like to keep it that way. class WhatIfArchiveParser(HTMLParser.HTMLParser): """ The WhatIfArchiveParser is a subclass of the Python standard library HTML parser. It is invoked by :func:`getWhatIfArchive` to parse the xkcd What If archive page, and automatically populate :class:`WhatIf` objects As there is not a JSON API for the What If blog (or at least, the author was unable to find one), this seemed the simplest way to implement fetching of information about them. This class is designed for internal usage only; there should be no reason for you to use it directly outside of the xkcd module. """ def __init__(self): # Ugh, this is an "old style class" if sys.version_info[0] <= 2: HTMLParser.HTMLParser.__init__(self) else: # Keep python 3.3 compatibility if sys.version_info[1] <= 3: super().__init__() else: super().__init__(convert_charrefs=False) # Create a dictionary of what-ifs, indexed by number. self.whatifs = {} self.currentWhatIf = None # Parsing metadata self.parsingWhatIf = False self.seenATag = 0 def handle_starttag(self, tag, attrs): # Check if this is an archive entry. if tag == "div" and ("class", "archive-entry") in attrs: self.parsingWhatIf = True self.currentWhatIf = WhatIf() # If we're parsing an archive entry: if self.parsingWhatIf: if tag == "a": # tags occur twice in an archive entry, this value influences the result of # the data parsed; is it an image or is it the title? self.seenATag += 1 # Only do this once. if self.currentWhatIf.number == -1: link = "" for pair in attrs: if pair[0] == "href": link = pair[1] # If we fail to find a link for whatever reason or if the parsing fails, # fail to generate a comic. try: num = link[len("//what-if.xkcd.com/"):-1] num = int(num) except: num = -1 self.currentWhatIf.number = num self.currentWhatIf.link = "https:" + link def handle_data(self, data): # Some cruder parsing to pick out the data. if self.parsingWhatIf: if self.seenATag == 2: self.currentWhatIf.title = data def handle_endtag(self, tag): # When we encounter the final , stop parsing these. if tag == "div" and self.parsingWhatIf: self.parsingWhatIf = False if self.currentWhatIf.number != -1: self.whatifs[self.currentWhatIf.number] = copy.copy(self.currentWhatIf) # When we encounter the final , reset seen counter to make handle_data # not do anything. if self.parsingWhatIf and tag == "a" and self.seenATag == 2: self.seenATag = 0 def getWhatIfs(self): """ Returns a dictionary of :class:`WhatIf` objects, indexed into by their number. This function must be invoked after the HTML parsing has finished, i.e. after calling self.feed. If for some reason the parsing has failed, the dictionary will be empty.""" return self.whatifs class Comic: """ Class representing a single xkcd comic. These can be produced via number of ways; if you know the number of the comic you want to query, you can just construct them yourself (e.g. Comic(integer)), but the recommended way is to use the :func:`getComic` function. There are also helper functions available to get the latest comic (:func:`getLatestComic`) and a random comic(:func:`getRandomComic`) as comic objects. """ def __init__(self, number): global xkcdUrl, imageUrl if type(number) is str and number.isdigit(): number = int(number) self.number = number if number <= 0: self.link = "Invalid comic" return """ The link to the comic on the xkcd website.""" self.link = xkcdUrl + str(number) #Get data from the JSON interface jsonString = self.link + "/info.0.json" xkcd = urllib.urlopen(jsonString).read() xkcdData = json.loads(xkcd.decode()) self.title = xkcdData['safe_title'] self.altText = xkcdData['alt'] self.imageLink = xkcdData['img'] # This may no longer be necessary. # if sys.version_info[0] >= 3: # self.title = str(self.title, encoding='UTF-8') # self.altText = str(self.altText, encoding='UTF-8') # self.imageLink = str(self.imageLink, encoding='UTF-8') #Get the image filename offset = len(imageUrl) index = self.imageLink.find(imageUrl) self.imageName = self.imageLink[index + offset:] def __str__(self): return "Comic object for " + self.link def __repr__(self): return "Comic object for " + self.link def getTitle(self): """ Returns the title of the comic, as a UTF-8 formatted Unicode string.""" return self.title def getAsciiTitle(self): """ Returns the ASCII-formatted version of the title. This function, and the other ASCII getters in the Comic class, exists so that code which depends on some legacy Python 2 component, like Twisted (which as of this writing does not support Unicode terribly well) can retrieve a version of comic metadata that they can use. It uses the :func:`convertToAscii` helper function to replace characters Python cannot automatically convert with a "?". You should do your best to not need to use this routine and prefer :func:`getTitle` wherever possible. """ asciiTitle = convertToAscii(self.title) return asciiTitle def getAsciiAltText(self): """ Returns the ASCII-formatted version of the comic's alt-text. See :func:`getAsciiTitle` and :func:`getAltText` for more information.""" asciiAltText = convertToAscii(self.altText) return asciiAltText def getAsciiImageLink(self): """ Returns the ASCII-formatted version of the link to the comic's image. See :func:`getAsciiTitle` and :func:`getImageLink` for more information.""" asciiImageLink = convertToAscii(self.imageLink) return asciiImageLink def getAltText(self): """ Returns the alt-text of the comic (the text that appears when one places their cursor over the image in a web browser) as a UTF-8 formatted Unicode string.""" return self.altText def getImageLink(self): """ Returns a URL linking to the comic's image as a UTF-8 formatted Unicode string.""" return self.imageLink def getImageName(self): """ Returns the filename of the comic's image as a UTF-8 formatted Unicode string.""" return self.imageName def getExplanation(self): """ Returns an explainxkcd link for the comic. explainxkcd is a wiki with community contributed explanations for xkcd comics; this function produces the URL for a given comic and returns that URL.""" global explanationUrl return explanationUrl + str(self.number) def show(self): """ Uses the Python webbrowser module to open the comic in your system's web browser.""" webbrowser.open_new_tab(self.link) def download(self, output="", outputFile="", silent=True): """ Downloads the image of the comic onto your computer. Arguments: output: the output directory where comics will be downloaded to. The default argument for 'output is the empty string; if the empty string is passed, it defaults to a "Downloads" directory in your home folder (this directory will be created if it does not exist). outputFile: the filename that will be written. If the empty string is passed, outputFile will default to a string of the form xkcd-(comic number)-(image filename), so for example, xkcd-1691-optimization.png. silent: boolean, defaults to True. If set to False, an error will be printed to standard output should the provided integer argument not be valid. Returns the path to the downloaded file, or an empty string in the event of failure.""" image = urllib.urlopen(self.imageLink).read() #Process optional input to work out where the dowload will go and what it'll be called if output != "": output = os.path.abspath(os.path.expanduser(output)) if output == "" or not os.path.exists(output): output = os.path.expanduser(os.path.join("~", "Downloads")) # Create ~/Downloads if it doesn't exist, since this is the default path. if not os.path.exists(output): os.mkdir(output) if outputFile == "": outputFile = "xkcd-" + str(self.number) + "-" + self.imageName output = os.path.join(output, outputFile) try: download = open(output, 'wb') except: if not silent: print("Unable to make file " + output) return "" download.write(image) download.close() return output # Functions that work on Comics. def getLatestComicNum(): """ Uses the xkcd JSON API to look up the number of the latest xkcd comic. Returns that number as an integer.""" xkcd = urllib.urlopen("https://xkcd.com/info.0.json").read() xkcdJSON = json.loads(xkcd.decode()) number = xkcdJSON['num'] return number def getLatestComic(): """ Produces a :class:`Comic` object for the latest xkcd comic. This function is just a wrapper around a call to :func:`getLatestComicNum`, and then constructs a :class:`Comic` object on its return value. Returns the resulting comic object.""" number = getLatestComicNum() return Comic(number) def getRandomComic(): """ Produces a :class:`Comic` object for a random xkcd comic. Uses the Python standard library random number generator in order to select a comic. Returns the resulting comic object.""" random.seed() numComics = getLatestComicNum() number = random.randint(1, numComics) return Comic(number) def getComic(number, silent=True): """ Produces a :class:`Comic` object with index equal to the provided argument. Prints an error in the event of a failure (i.e. the number is less than zero or greater than the latest comic number) and returns an empty Comic object. Arguments: an integer or string that represents a number, "number", that is the index of the comic in question. silent: boolean, defaults to True. If set to False, an error will be printed to standard output should the provided integer argument not be valid. Returns the resulting Comic object for the provided index if successful, or a Comic object with -1 as the index if not.""" numComics = getLatestComicNum() if type(number) is str and number.isdigit(): number = int(number) if number > numComics or number <= 0: if not silent: print("Error: You have requested an invalid comic.") return Comic(-1) return Comic(number) # Functions that work on What Ifs. def getWhatIfArchive(): """ Parses the xkcd What If archive. getWhatIfArchive passes the HTML text of the archive page into a :class:`WhatIfArchiveParser` and then calls the parser's :func:`WhatIfArchiveParser.getWhatIfs` method and returns the dictionary produced. This function returns a dictionary mapping article numbers to :class:`WhatIf` objects for every What If article published thus far. If the parsing fails, for whatever reason, the dictionary will be empty.""" archive = urllib.urlopen(archiveUrl) text = archive.read() if sys.version_info[0] >= 3: text = text.decode('utf-8') archive.close() parser = WhatIfArchiveParser() parser.feed(text) return parser.getWhatIfs() def getLatestWhatIfNum(archive=None): """ Returns an integer representing the number of the latest What If article published. This is done by calling :class:`getLatestWhatIf` and returning the number of that method's result. Takes an optional "archive" argument. If this argument is None, the :func:`getWhatIfArchive` routine is first called to populate the archive of published What If articles. If it is not, however, "archive" is assumed to be a dictionary and used as the set of articles to chooose from. """ latestWhatIf = getLatestWhatIf(archive) return latestWhatIf.number def getLatestWhatIf(archive=None): """ Returns a :class:`WhatIf` object representing the latest What If article. Takes an optional "archive" argument. If this argument is None, the :func:`getWhatIfArchive` routine is first called to populate the archive of published What If articles. If it is not, however, "archive" is assumed to be a dictionary and used as the set of articles to chooose from. """ if archive is None: archive = getWhatIfArchive() # Get the archive keys as a list and sort them by ascending order. # The last entry in keys will be the latest What if. keys = list(archive.keys()) keys.sort() return archive[keys[-1]] def getRandomWhatIf(): """ Returns a randomly generated :class:`WhatIf` object, using the Python standard library random number generator to select the object. The object is returned from the dictionary produced by :func:`getWhatIfArchive`; like the other What If routines, this function is called first in order to get a list of all previously published What Ifs.""" random.seed() archive = getWhatIfArchive() latest = getLatestWhatIfNum(archive) number = random.randint(1, latest) return archive[number] def getWhatIf(number): """ Returns a :class:`WhatIf` object corresponding to the What If article of index passed to the function. If the index is less than zero or greater than the maximum number of articles published thus far, None is returned instead. Like all the routines for handling What If articles, :func:`getWhatIfArchive` is called first in order to establish a list of all previously published What Ifs. Arguments: number: an integer or string that represents a number, this is the index of article to retrieve. Returns the resulting :class:`WhatIf` object.""" archive = getWhatIfArchive() latest = getLatestWhatIfNum(archive) if type(number) is str and number.isdigit(): number = int(number) if number > latest or latest <= 0: return None return archive[number] # Utility functions def convertToAscii(string, error="?"): """ Utility function that converts a unicode string to ASCII. This exists so the :class:`Comic` class can be compatible with Python 2 libraries that expect ASCII strings, such as Twisted (as of this writing, anyway). It is unlikely something you will need directly, and its use is discouraged. Arguments: string: the string to attempt to convert. error: a string that will be substituted into 'string' wherever Python is unable to automatically do the conversion. convertToAscii returns the converted string.""" running = True asciiString = string while running: try: asciiString = asciiString.encode('ascii') except UnicodeError as unicode: start = unicode.start end = unicode.end asciiString = asciiString[:start] + "?" + asciiString[end:] else: running = False return asciiString