diff --git a/.gitignore b/.gitignore index 96d23a3..f465139 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ dist/ .tox/ _public/ tests/functional/fixtures/recording-*.json +#* +*#* diff --git a/.travis.yml b/.travis.yml index 56eee30..c924bfd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: python +sudo: false python: - "2.6" - "2.7" @@ -14,7 +15,7 @@ env: - TEST_TYPE=functional install: - - pip install -r requirements.txt -r test-requirements.txt + - pip install -r development.txt script: - make $TEST_TYPE diff --git a/Makefile b/Makefile index 19c563d..e9aa9ce 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,14 @@ -all: check_dependencies unit functional doctests +# Config +OSNAME := $(shell uname) + +ifeq ($(OSNAME), Linux) +OPEN_COMMAND := gnome-open +else +OPEN_COMMAND := open +endif + + +all: check_dependencies unit functional acceptance filename=httpretty-`python -c 'import httpretty;print httpretty.version'`.tar.gz @@ -11,17 +21,21 @@ check_dependencies: python -c "import $$dependency" 2>/dev/null || (echo "You must install $$dependency in order to run httpretty's tests" && exit 3) ; \ done -test: unit functional doctests +test: unit functional acceptance -unit: prepare +lint: + @echo "Checking code style ..." + @flake8 httpretty + +unit: prepare lint @echo "Running unit tests ..." - @nosetests -s tests/unit + @nosetests --rednose -x --with-randomly --with-coverage --cover-package=httpretty -s tests/unit functional: prepare @echo "Running functional tests ..." - @nosetests -s tests/functional + @nosetests --rednose -x --with-randomly --with-coverage --cover-package=httpretty -s tests/functional -doctests: prepare +acceptance: prepare @echo "Running documentation tests tests ..." @steadymark README.md @@ -35,9 +49,9 @@ release: clean unit functional @./.release @python setup.py sdist bdist_wheel register upload -docs: doctests - @pandoc -o README.rst README.md - @markment -o . -t ./theme --sitemap-for="http://falcao.it/HTTPretty" docs +docs: acceptance + @cd docs && make html + $(OPEN_COMMAND) docs/build/html/index.html deploy-docs: @git co master && \ @@ -51,3 +65,6 @@ deploy-docs: prepare: @reset + + +.PHONY: docs diff --git a/README.md b/README.md index 2227f42..bda5fc4 100644 --- a/README.md +++ b/README.md @@ -508,7 +508,7 @@ make unit functional # License - Copyright (C) <2011-2013> Gabriel Falcão + Copyright (C) <2011-2015> Gabriel Falcão Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/README.rst b/README.rst index 9fd3efe..14a727c 100644 --- a/README.rst +++ b/README.rst @@ -551,7 +551,7 @@ License :: - Copyright (C) <2011-2013> Gabriel Falcão + Copyright (C) <2011-2015> Gabriel Falcão Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index 7aef4d4..0000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ --r requirements.txt -coverage -tox -markment -steadymark -nose -rednose -mock -sure \ No newline at end of file diff --git a/development.txt b/development.txt new file mode 100644 index 0000000..86c36aa --- /dev/null +++ b/development.txt @@ -0,0 +1,17 @@ +# Test dependencies, both for local environment and CI server +flake8==2.5.1 +httplib2==0.9.2 +ipdb==0.8.1 +misaka==2.0.0 +mock==1.3.0 +nose==1.3.7 +nose-randomly==1.2.0 +rednose==0.4.3 +requests==2.8.1 +steadymark==0.7.1 +sure==1.2.24 +urllib3==1.12 +tornado==4.3 +coverage==4.0.3 +Sphinx==1.3.3 +sphinx-rtd-theme==0.1.9 diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..e4cb84b --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,192 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# 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 coverage 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 " applehelp to make an Apple Help Book" + @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 " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @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 " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of 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/HTTPretty.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/HTTPretty.qhc" + +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/HTTPretty" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/HTTPretty" + @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." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @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." + +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/contributing.md b/docs/contributing.md index 8b2c112..95d9a95 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -28,7 +28,7 @@ make unit functional # License - Copyright (C) <2011-2013> Gabriel Falcão + Copyright (C) <2011-2015> Gabriel Falcão Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/docs/source/acks.rst b/docs/source/acks.rst new file mode 100644 index 0000000..3543d0c --- /dev/null +++ b/docs/source/acks.rst @@ -0,0 +1,21 @@ +Acknowledgements +############### + +caveats +======= + +``forcing_headers`` + ``Content-Length`` +---------------------------------------- +if you use the ``forcing_headers`` options make sure to add the header +``Content-Length`` otherwise the +[requests](http://docs.python-requests.org/en/latest/) will try to +load the response endlessly + +supported libraries +------------------- + +Because HTTPretty works in the socket level it should work with any HTTP client libraries, although it is `battle tested `_ against: + +* `requests `_ +* `httplib2 `_ +* `urllib2 `_ diff --git a/docs/source/api-reference.rst b/docs/source/api-reference.rst new file mode 100644 index 0000000..67ac171 --- /dev/null +++ b/docs/source/api-reference.rst @@ -0,0 +1,20 @@ +.. _API Reference: + + +API Reference +============= + +.. automodule:: httpretty.core + :members: + +.. automodule:: httpretty.http + :members: + +.. automodule:: httpretty.utils + :members: + +.. automodule:: httpretty.errors + :members: + +.. automodule:: httpretty.compat + :members: diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..e1ad3df --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,368 @@ +# -*- coding: utf-8 -*- +# +# HTTPretty documentation build configuration file, created by +# sphinx-quickstart on Sun Dec 13 07:25:00 2015. +# +# 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 +import os +import shlex + +# 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.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +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'HTTPretty' +copyright = u'2015, Gabriel Falcão' +author = u'Gabriel Falcão' + +# 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 = u'0.8.10' +# The full version, including alpha/beta/rc tags. +release = u'0.8.10' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +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 = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +import sphinx_rtd_theme +html_theme = "sphinx_rtd_theme" +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# 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'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# 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 + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'HTTPrettydoc' + +# -- 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': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'HTTPretty.tex', u'HTTPretty Documentation', + u'Gabriel Falcão', '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 = [ + (master_doc, 'httpretty', u'HTTPretty Documentation', + [author], 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 = [ + (master_doc, 'HTTPretty', u'HTTPretty Documentation', + author, 'HTTPretty', 'One line description of project.', + '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' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + + +# -- Options for Epub output ---------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project +epub_author = author +epub_publisher = author +epub_copyright = copyright + +# The basename for the epub file. It defaults to the project name. +#epub_basename = project + +# The HTML theme for the epub output. Since the default themes are not optimized +# for small screen space, using the same theme for HTML and epub output is +# usually not wise. This defaults to 'epub', a theme designed to save visual +# space. +#epub_theme = 'epub' + +# The language of the text. It defaults to the language option +# or 'en' if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# A tuple containing the cover image and cover page html template filenames. +#epub_cover = () + +# A sequence of (type, uri, title) tuples for the guide element of content.opf. +#epub_guide = () + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 + +# Allow duplicate toc entries. +#epub_tocdup = True + +# Choose between 'default' and 'includehidden'. +#epub_tocscope = 'default' + +# Fix unsupported image types using the Pillow. +#epub_fix_images = False + +# Scale large images. +#epub_max_image_width = 0 + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#epub_show_urls = 'inline' + +# If false, no index is generated. +#epub_use_index = True + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..1f3e241 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,23 @@ +.. HTTPretty documentation master file, created by + sphinx-quickstart on Sun Dec 13 07:25:00 2015. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to HTTPretty's documentation! +===================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + tutorial + acks + api-reference + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst new file mode 100644 index 0000000..663e056 --- /dev/null +++ b/docs/source/tutorial.rst @@ -0,0 +1,100 @@ +What is HTTPretty ? +################### + +.. highlight:: python + +Once upon a time a python developer wanted to use a RESTful api, +everything was fine but until the day he needed to test the code that +hits the RESTful API: what if the API server is down? What if its +content has changed ? + +Don't worry, HTTPretty is here for you: + +:: + + import requests + from sure import expect + import httpretty + + + @httpretty.activate + def test_yipit_api_returning_deals(): + httpretty.register_uri(httpretty.GET, "http://api.yipit.com/v1/deals/", + body='[{"title": "Test Deal"}]', + content_type="application/json") + + response = requests.get('http://api.yipit.com/v1/deals/') + + expect(response.json()).to.equal([{"title": "Test Deal"}]) + + +A more technical description +============================ + +HTTPretty is a HTTP client mock library for Python 100% inspired on ruby's [FakeWeb](http://fakeweb.rubyforge.org/). +If you come from ruby this would probably sound familiar :smiley: + +Installing +========== + +Installing httpretty is as easy as: + +.. highlight:: bash + +:: + + pip install HTTPretty + + +Demo +#### + +expecting a simple response body +================================ + +.. highlight:: python + +:: + + import requests + import httpretty + + def test_one(): + httpretty.enable() # enable HTTPretty so that it will monkey patch the socket module + httpretty.register_uri(httpretty.GET, "http://yipit.com/", + body="Find the best daily deals") + + response = requests.get('http://yipit.com') + + assert response.text == "Find the best daily deals" + + httpretty.disable() # disable afterwards, so that you will have no problems in code that uses that socket module + httpretty.reset() # reset HTTPretty state (clean up registered urls and request history) + + +Motivation +########## + +When building systems that access external resources such as RESTful +webservices, XMLRPC or even simple HTTP requests, we stumble in the +problem: + + *"I'm gonna need to mock all those requests"* + +It brings a lot of hassle, you will need to use a generic mocking +tool, mess with scope and so on. + +The idea behind HTTPretty (how it works) +======================================== + + +HTTPretty `monkey patches `_ +Python's `socket `_ core +module, reimplementing the HTTP protocol, by mocking requests and +responses. + +As for how it works this way, you don't need to worry what http +library you're gonna use. + +HTTPretty will mock the response for you :) *(and also give you the +latest requests so that you can check them)* diff --git a/httpretty/__init__.py b/httpretty/__init__.py index a752b45..3b2259c 100644 --- a/httpretty/__init__.py +++ b/httpretty/__init__.py @@ -1,7 +1,7 @@ # #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -55,6 +55,33 @@ def last_request(): """returns the last request""" return httpretty.last_request + def has_request(): """returns a boolean indicating whether any request has been made""" return not isinstance(httpretty.last_request.headers, EmptyRequestHeaders) + + +__all__ = [ + 'last_request', + 'has_request', + 'GET', + 'PUT', + 'POST', + 'DELETE', + 'HEAD', + 'PATCH', + 'OPTIONS', + 'CONNECT', + 'register_uri', + 'enable', + 'disable', + 'is_enabled', + 'reset', + 'Response', + 'URIInfo', + 'UnmockedError', + 'HTTPrettyError', + 'httpretty', + 'httprettified', + 'EmptyRequestHeaders', +] diff --git a/httpretty/compat.py b/httpretty/compat.py index 6805cf6..803b55d 100644 --- a/httpretty/compat.py +++ b/httpretty/compat.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -37,9 +37,6 @@ if PY3: # pragma: no cover StringIO = io.BytesIO basestring = (str, bytes) - class BaseClass(object): - def __repr__(self): - return self.__str__() else: # pragma: no cover text_type = unicode byte_type = str @@ -49,6 +46,7 @@ else: # pragma: no cover class BaseClass(object): + def __repr__(self): ret = self.__str__() if PY3: # pragma: no cover @@ -58,11 +56,17 @@ class BaseClass(object): try: # pragma: no cover - from urllib.parse import urlsplit, urlunsplit, parse_qs, quote, quote_plus, unquote + from urllib.parse import urlsplit + from urllib.parse import urlunsplit + from urllib.parse import parse_qs + from urllib.parse import quote + from urllib.parse import quote_plus + from urllib.parse import unquote unquote_utf8 = unquote except ImportError: # pragma: no cover from urlparse import urlsplit, urlunsplit, parse_qs, unquote from urllib import quote, quote_plus + def unquote_utf8(qs): if isinstance(qs, text_type): qs = qs.encode('utf-8') diff --git a/httpretty/core.py b/httpretty/core.py index bcd69b2..6d9870d 100644 --- a/httpretty/core.py +++ b/httpretty/core.py @@ -1,7 +1,7 @@ # #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -32,7 +32,6 @@ import socket import functools import itertools import warnings -import logging import traceback import json import contextlib @@ -43,6 +42,7 @@ from .compat import ( PY3, StringIO, text_type, + byte_type, BaseClass, BaseHTTPRequestHandler, quote, @@ -50,7 +50,6 @@ from .compat import ( urlunsplit, urlsplit, parse_qs, - unquote, unquote_utf8, ClassTypes, basestring @@ -84,8 +83,9 @@ old_ssl_wrap_socket = None old_sslwrap_simple = None old_sslsocket = None -if PY3: # pragma: no cover - basestring = (bytes, str) +MULTILINE_ANY_REGEX = re.compile(r'.*', re.M) + + try: # pragma: no cover import socks old_socksocket = socks.socksocket @@ -109,7 +109,7 @@ POTENTIAL_HTTPS_PORTS = set(DEFAULT_HTTPS_PORTS) class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass): - """Represents a HTTP request. It takes a valid multi-line, `\r\n` + """Represents a HTTP request. It takes a valid multi-line, ``\r\n`` separated string with HTTP headers and parse them out using the internal `parse_request` method. @@ -119,26 +119,26 @@ class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass): It has some convenience attributes: - `headers` -> a mimetype object that can be cast into a dictionary, + ``headers`` -> a mimetype object that can be cast into a dictionary, contains all the request headers - `method` -> the HTTP method used in this request + ``method`` -> the HTTP method used in this request - `querystring` -> a dictionary containing lists with the + ``querystring`` -> a dictionary containing lists with the attributes. Please notice that if you need a single value from a query string you will need to get it manually like: - ```python - >>> request.querystring - {'name': ['Gabriel Falcao']} - >>> print request.querystring['name'][0] - ``` + :: - `parsed_body` -> a dictionary containing parsed request body or + >>> request.querystring + {'name': ['Gabriel Falcao']} + >>> print request.querystring['name'][0] + + ``parsed_body`` -> a dictionary containing parsed request body or None if HTTPrettyRequest doesn't know how to parse it. It currently supports parsing body data that was sent under the - `content-type` headers values: 'application/json' or - 'application/x-www-form-urlencoded' + ``content`-type` headers values: ``application/json`` or + ``application/x-www-form-urlencoded`` """ def __init__(self, headers, body=''): # first of all, lets make sure that if headers or body are @@ -150,9 +150,10 @@ class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass): # Now let's concatenate the headers with the body, and create # `rfile` based on it self.rfile = StringIO(b'\r\n\r\n'.join([self.raw_headers, self.body])) - self.wfile = StringIO() # Creating `wfile` as an empty - # StringIO, just to avoid any real - # I/O calls + + # Creating `wfile` as an empty StringIO, just to avoid any + # real I/O calls + self.wfile = StringIO() # parsing the request line preemptively self.raw_requestline = self.rfile.readline() @@ -185,8 +186,12 @@ class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass): # `application/json` or `application/x-www-form-urlencoded` self.parsed_body = self.parse_request_body(self.body) + def __nonzero__(self): + return bool(self.body) or bool(self.raw_headers) + def __str__(self): - return ''.format( + tmpl = '' + return tmpl.format( self.headers.get('content-type', ''), len(self.headers), len(self.body), @@ -202,7 +207,8 @@ class HTTPrettyRequest(BaseHTTPRequestHandler, BaseClass): return result def parse_request_body(self, body): - """ Attempt to parse the post based on the content-type passed. Return the regular body if not """ + """Attempt to parse the post based on the content-type passed. + Return the regular body if not""" PARSING_FUNCTIONS = { 'application/json': json.loads, @@ -251,15 +257,15 @@ class fakesock(object): _sent_data = [] def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, - protocol=0): + protocol=0, _sock=None): self.truesock = (old_socket(family, type, protocol) if httpretty.allow_net_connect else None) self._closed = True self.fd = FakeSockFile() - self.fd.socket = self + self.fd.socket = _sock or self self.timeout = socket._GLOBAL_DEFAULT_TIMEOUT - self._sock = self + self._sock = _sock or self self.is_http = False self._bufsize = 1024 @@ -306,7 +312,9 @@ class fakesock(object): # See issue #206 self.is_http = False else: - self.is_http = self._port in POTENTIAL_HTTP_PORTS | POTENTIAL_HTTPS_PORTS + ports_to_check = ( + POTENTIAL_HTTP_PORTS.union(POTENTIAL_HTTPS_PORTS)) + self.is_http = self._port in ports_to_check if not self.is_http: if self.truesock: @@ -371,7 +379,7 @@ class fakesock(object): try: received = self.truesock.recv(self._bufsize) self.fd.write(received) - should_continue = len(received) == self._bufsize + should_continue = bool(received.strip()) except socket.error as e: if e.errno == EAGAIN: @@ -386,14 +394,20 @@ class fakesock(object): self.fd.socket = self try: requestline, _ = data.split(b'\r\n', 1) - method, path, version = parse_requestline(decode_utf8(requestline)) + method, path, version = parse_requestline( + decode_utf8(requestline)) is_parsing_headers = True except ValueError: + path = '' is_parsing_headers = False - if not self._entry: - # If the previous request wasn't mocked, don't mock the subsequent sending of data + if self._entry is None: + # If the previous request wasn't mocked, don't + # mock the subsequent sending of data return self.real_sendall(data, *args, **kw) + else: + method = self._entry.method + path = self._entry.info.path self.fd.seek(0) @@ -403,7 +417,11 @@ class fakesock(object): meta = self._entry.request.headers body = utf8(self._sent_data[-1]) if meta.get('transfer-encoding', '') == 'chunked': - if not body.isdigit() and body != b'\r\n' and body != b'0\r\n\r\n': + if ( + not body.isdigit() + and (body != b'\r\n') + and (body != b'0\r\n\r\n') + ): self._entry.request.body += body else: self._entry.request.body += body @@ -414,14 +432,22 @@ class fakesock(object): # path might come with s = urlsplit(path) POTENTIAL_HTTP_PORTS.add(int(s.port or 80)) - headers, body = list(map(utf8, data.split(b'\r\n\r\n', 1))) + parts = list(map(utf8, data.split(b'\r\n\r\n', 1))) + if len(parts) == 2: + headers, body = parts + else: + headers = '' + body = data request = httpretty.historify_request(headers, body) - info = URIInfo(hostname=self._host, port=self._port, - path=s.path, - query=s.query, - last_request=request) + info = URIInfo( + hostname=self._host, + port=self._port, + path=s.path, + query=s.query, + last_request=request + ) matcher, entries = httpretty.match_uriinfo(info) @@ -480,7 +506,10 @@ def fake_wrap_socket(s, *args, **kw): return s -def create_fake_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, source_address=None): +def create_fake_connection( + address, + timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + source_address=None): s = fakesock.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: s.settimeout(timeout) @@ -555,15 +584,15 @@ class Entry(BaseClass): igot = int(got) except (ValueError, TypeError): warnings.warn( - 'HTTPretty got to register the Content-Length header ' \ + 'HTTPretty got to register the Content-Length header ' 'with "%r" which is not a number' % got) return if igot > self.body_length: raise HTTPrettyError( - 'HTTPretty got inconsistent parameters. The header ' \ - 'Content-Length you registered expects size "%d" but ' \ - 'the body you registered for that has actually length ' \ + 'HTTPretty got inconsistent parameters. The header ' + 'Content-Length you registered expects size "%d" but ' + 'the body you registered for that has actually length ' '"%d".' % ( igot, self.body_length, ) @@ -595,12 +624,18 @@ class Entry(BaseClass): headers = self.forcing_headers if self.adding_headers: - headers.update(self.normalize_headers(self.adding_headers)) + headers.update( + self.normalize_headers( + self.adding_headers)) headers = self.normalize_headers(headers) status = headers.get('status', self.status) if self.body_is_callable: - status, headers, self.body = self.callable_body(self.request, self.info.full_url(), headers) + status, headers, self.body = self.callable_body( + self.request, + self.info.full_url(), + headers + ) headers.update({ 'content-length': len(self.body) }) @@ -616,7 +651,8 @@ class Entry(BaseClass): content_type = headers.pop('content-type', 'text/plain; charset=utf-8') - content_length = headers.pop('content-length', self.body_length) + content_length = headers.pop('content-length', + byte_type(self.body_length)) string_list.append('content-type: %s' % content_type) if not self.streaming: @@ -778,7 +814,7 @@ class URIMatcher(object): self.entries = entries self.priority = priority - #hash of current_entry pointers, per method. + # hash of current_entry pointers, per method. self.current_entries = {} def matches(self, info): @@ -802,7 +838,8 @@ class URIMatcher(object): if method not in self.current_entries: self.current_entries[method] = 0 - #restrict selection to entries that match the requested method + # restrict selection to entries that match the requested + # method entries_for_method = [e for e in self.entries if e.method == method] if self.current_entries[method] >= len(entries_for_method): @@ -860,13 +897,17 @@ class httpretty(HttpBaseClass): try: import urllib3 except ImportError: - raise RuntimeError('HTTPretty requires urllib3 installed for recording actual requests.') - + msg = ( + 'HTTPretty requires urllib3 installed ' + 'for recording actual requests.' + ) + raise RuntimeError(msg) http = urllib3.PoolManager() cls.enable() calls = [] + def record_request(request, uri, headers): cls.disable() @@ -889,7 +930,7 @@ class httpretty(HttpBaseClass): return response.status, response.headers, response.data for method in cls.METHODS: - cls.register_uri(method, re.compile(r'.*', re.M), body=record_request) + cls.register_uri(method, MULTILINE_ANY_REGEX, body=record_request) yield cls.disable() @@ -905,7 +946,9 @@ class httpretty(HttpBaseClass): for item in data: uri = item['request']['uri'] method = item['request']['method'] - cls.register_uri(method, uri, body=item['response']['body'], forcing_headers=item['response']['headers']) + body = item['response']['body'] + headers = item['response']['headers'] + cls.register_uri(method, uri, body=body, forcing_headers=headers) yield cls.disable() @@ -969,8 +1012,15 @@ class httpretty(HttpBaseClass): return '' % len(self._entries) @classmethod - def Response(cls, body, method=None, uri=None, adding_headers=None, forcing_headers=None, - status=200, streaming=False, **headers): + def Response( + cls, body, + method=None, + uri=None, + adding_headers=None, + forcing_headers=None, + status=200, + streaming=False, + **headers): headers[str('body')] = body headers[str('adding_headers')] = adding_headers diff --git a/httpretty/errors.py b/httpretty/errors.py index cb6479b..c2abaef 100644 --- a/httpretty/errors.py +++ b/httpretty/errors.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation diff --git a/httpretty/http.py b/httpretty/http.py index 7e9a568..90fde93 100644 --- a/httpretty/http.py +++ b/httpretty/http.py @@ -1,7 +1,7 @@ # #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation diff --git a/httpretty/utils.py b/httpretty/utils.py index caa8fa1..bb75bf7 100644 --- a/httpretty/utils.py +++ b/httpretty/utils.py @@ -1,7 +1,7 @@ # #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation diff --git a/requirements.txt b/requirements.txt index e69de29..521c9c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1 @@ +# HTTPretty doesn't have any requirements per se so far. yay! diff --git a/tests/functional/__init__.py b/tests/functional/__init__.py index 8589d02..5091429 100644 --- a/tests/functional/__init__.py +++ b/tests/functional/__init__.py @@ -1,7 +1,7 @@ # #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation diff --git a/tests/functional/base.py b/tests/functional/base.py index 4808752..7fe7952 100644 --- a/tests/functional/base.py +++ b/tests/functional/base.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -26,16 +26,28 @@ # OTHER DEALINGS IN THE SOFTWARE. from __future__ import unicode_literals + import os +import json +import socket import threading -import traceback + import tornado.ioloop import tornado.web from functools import wraps -from sure import scenario -import json + from os.path import abspath, dirname, join -from httpretty.core import POTENTIAL_HTTP_PORTS +from httpretty.core import POTENTIAL_HTTP_PORTS, old_socket + + +def get_free_tcp_port(): + """returns a TCP port that can be used for listen in the host. + """ + tcp = old_socket(socket.AF_INET, socket.SOCK_STREAM) + tcp.bind(('', 0)) + host, port = tcp.getsockname() + tcp.close() + return port LOCAL_FILE = lambda *path: join(abspath(dirname(__file__)), *path) @@ -53,7 +65,7 @@ class JSONEchoHandler(tornado.web.RequestHandler): class JSONEchoServer(threading.Thread): - def __init__(self, lock, port=8888, *args, **kw): + def __init__(self, lock, port, *args, **kw): self.lock = lock self.port = int(port) self._stop = threading.Event() @@ -78,14 +90,16 @@ class JSONEchoServer(threading.Thread): tornado.ioloop.IOLoop.instance().start() - def use_tornado_server(callback): lock = threading.Lock() lock.acquire() @wraps(callback) def func(*args, **kw): - server = JSONEchoServer(lock, os.getenv('TEST_PORT', 8888)) + port = os.getenv('TEST_PORT', get_free_tcp_port()) + POTENTIAL_HTTP_PORTS.add(port) + kw['port'] = port + server = JSONEchoServer(lock, port) server.start() try: lock.acquire() @@ -93,6 +107,6 @@ def use_tornado_server(callback): finally: lock.release() server.stop() - if 8888 in POTENTIAL_HTTP_PORTS: - POTENTIAL_HTTP_PORTS.remove(8888) + if port in POTENTIAL_HTTP_PORTS: + POTENTIAL_HTTP_PORTS.remove(port) return func diff --git a/tests/functional/test_bypass.py b/tests/functional/test_bypass.py index 94c0c8a..8d4ec42 100644 --- a/tests/functional/test_bypass.py +++ b/tests/functional/test_bypass.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -25,13 +25,15 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. from __future__ import unicode_literals - +import time +import requests try: import urllib.request as urllib2 except ImportError: import urllib2 from .testserver import TornadoServer, TCPServer, TCPClient +from .base import get_free_tcp_port from sure import expect, that_with_context import functools @@ -41,8 +43,23 @@ from httpretty import core, HTTPretty def start_http_server(context): - context.server = TornadoServer(9999) + httpretty.disable() + context.http_port = get_free_tcp_port() + context.server = TornadoServer(context.http_port) context.server.start() + ready = False + timeout = 2 + started_at = time.time() + while not ready: + httpretty.disable() + time.sleep(.1) + try: + requests.get('http://localhost:{0}/'.format(context.http_port)) + ready = True + except: + if time.time() - started_at >= timeout: + break + httpretty.enable() @@ -52,9 +69,10 @@ def stop_http_server(context): def start_tcp_server(context): - context.server = TCPServer(8888) + context.tcp_port = get_free_tcp_port() + context.server = TCPServer(context.tcp_port) context.server.start() - context.client = TCPClient(8888) + context.client = TCPClient(context.tcp_port) httpretty.enable() @@ -70,19 +88,19 @@ def test_httpretty_bypasses_when_disabled(context): "httpretty should bypass all requests by disabling it" httpretty.register_uri( - httpretty.GET, "http://localhost:9999/go-for-bubbles/", + httpretty.GET, "http://localhost:{0}/go-for-bubbles/".format(context.http_port), body="glub glub") httpretty.disable() - fd = urllib2.urlopen('http://localhost:9999/go-for-bubbles/') + fd = urllib2.urlopen('http://localhost:{0}/go-for-bubbles/'.format(context.http_port)) got1 = fd.read() fd.close() expect(got1).to.equal( b'. o O 0 O o . o O 0 O o . o O 0 O o . o O 0 O o . o O 0 O o .') - fd = urllib2.urlopen('http://localhost:9999/come-again/') + fd = urllib2.urlopen('http://localhost:{0}/come-again/'.format(context.http_port)) got2 = fd.read() fd.close() @@ -90,12 +108,13 @@ def test_httpretty_bypasses_when_disabled(context): httpretty.enable() - fd = urllib2.urlopen('http://localhost:9999/go-for-bubbles/') + fd = urllib2.urlopen('http://localhost:{0}/go-for-bubbles/'.format(context.http_port)) got3 = fd.read() fd.close() expect(got3).to.equal(b'glub glub') - core.POTENTIAL_HTTP_PORTS.remove(9999) + core.POTENTIAL_HTTP_PORTS.remove(context.http_port) + @httpretty.activate @that_with_context(start_http_server, stop_http_server) @@ -103,21 +122,21 @@ def test_httpretty_bypasses_a_unregistered_request(context): "httpretty should bypass a unregistered request by disabling it" httpretty.register_uri( - httpretty.GET, "http://localhost:9999/go-for-bubbles/", + httpretty.GET, "http://localhost:{0}/go-for-bubbles/".format(context.http_port), body="glub glub") - fd = urllib2.urlopen('http://localhost:9999/go-for-bubbles/') + fd = urllib2.urlopen('http://localhost:{0}/go-for-bubbles/'.format(context.http_port)) got1 = fd.read() fd.close() expect(got1).to.equal(b'glub glub') - fd = urllib2.urlopen('http://localhost:9999/come-again/') + fd = urllib2.urlopen('http://localhost:{0}/come-again/'.format(context.http_port)) got2 = fd.read() fd.close() expect(got2).to.equal(b'<- HELLO WORLD ->') - core.POTENTIAL_HTTP_PORTS.remove(9999) + core.POTENTIAL_HTTP_PORTS.remove(context.http_port) @httpretty.activate @@ -163,7 +182,7 @@ def test_disallow_net_connect_1(context): def foo(): fd = None try: - fd = urllib2.urlopen('http://localhost:9999/go-for-bubbles/') + fd = urllib2.urlopen('http://localhost:{0}/go-for-bubbles/'.format(context.http_port)) finally: if fd: fd.close() diff --git a/tests/functional/test_debug.py b/tests/functional/test_debug.py index 1562be6..1276a68 100644 --- a/tests/functional/test_debug.py +++ b/tests/functional/test_debug.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation diff --git a/tests/functional/test_httplib2.py b/tests/functional/test_httplib2.py index 11baedc..eec2a69 100644 --- a/tests/functional/test_httplib2.py +++ b/tests/functional/test_httplib2.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation diff --git a/tests/functional/test_requests.py b/tests/functional/test_requests.py index 25cb539..5ce7f18 100644 --- a/tests/functional/test_requests.py +++ b/tests/functional/test_requests.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -52,8 +52,7 @@ except NameError: return it.next() next = advance_iterator -PORT = int(os.getenv('TEST_PORT') or 8888) -server_url = lambda path: "http://localhost:{0}/{1}".format(PORT, path.lstrip('/')) +server_url = lambda path, port: "http://localhost:{0}/{1}".format(port, path.lstrip('/')) @httprettified @@ -701,15 +700,15 @@ def test_unicode_querystrings(): @use_tornado_server -def test_recording_calls(): +def test_recording_calls(port): ("HTTPretty should be able to record calls") # Given a destination path: - destination = FIXTURE_FILE("recording-.json") + destination = FIXTURE_FILE("recording-1.json") # When I record some calls with HTTPretty.record(destination): - requests.get(server_url("/foobar?name=Gabriel&age=25")) - requests.post(server_url("/foobar"), data=json.dumps({'test': '123'})) + requests.get(server_url("/foobar?name=Gabriel&age=25", port)) + requests.post(server_url("/foobar", port), data=json.dumps({'test': '123'})) # Then the destination path should exist os.path.exists(destination).should.be.true @@ -741,19 +740,13 @@ def test_recording_calls(): response['response'].should.have.key("status").being.equal(200) response['response'].should.have.key("body").being.an(text_type) response['response'].should.have.key("headers").being.a(dict) - response['response']["headers"].should.have.key("server").being.equal("TornadoServer/" + tornado_version) + response['response']["headers"].should.have.key("Server").being.equal("TornadoServer/" + tornado_version) - -def test_playing_calls(): - ("HTTPretty should be able to record calls") - # Given a destination path: - destination = FIXTURE_FILE("playback-1.json") - - # When I playback some previously recorded calls + # And When I playback the previously recorded calls with HTTPretty.playback(destination): # And make the expected requests - response1 = requests.get(server_url("/foobar?name=Gabriel&age=25")) - response2 = requests.post(server_url("/foobar"), data=json.dumps({'test': '123'})) + response1 = requests.get(server_url("/foobar?name=Gabriel&age=25", port)) + response2 = requests.post(server_url("/foobar", port), data=json.dumps({'test': '123'})) # Then the responses should be the expected response1.json().should.equal({"foobar": {"age": "25", "name": "Gabriel"}}) diff --git a/tests/functional/test_urllib2.py b/tests/functional/test_urllib2.py index cb84f00..cd69356 100644 --- a/tests/functional/test_urllib2.py +++ b/tests/functional/test_urllib2.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation diff --git a/tests/functional/testserver.py b/tests/functional/testserver.py index db070ba..66cef51 100644 --- a/tests/functional/testserver.py +++ b/tests/functional/testserver.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index a5df75e..802f7bc 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -1,7 +1,7 @@ # #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 9249dbb..3dc988d 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -441,7 +441,7 @@ def test_fakesock_socket_real_sendall_when_http(POTENTIAL_HTTP_PORTS, old_socket # And the potential http port is 4000 POTENTIAL_HTTP_PORTS.__contains__.side_effect = lambda other: int(other) == 4000 - POTENTIAL_HTTP_PORTS.__or__.side_effect = lambda other: POTENTIAL_HTTP_PORTS + POTENTIAL_HTTP_PORTS.union.side_effect = lambda other: POTENTIAL_HTTP_PORTS # Given a fake socket socket = fakesock.socket() @@ -472,8 +472,8 @@ def test_fakesock_socket_real_sendall_when_http(POTENTIAL_HTTP_PORTS, old_socket @patch('httpretty.core.POTENTIAL_HTTP_PORTS') def test_fakesock_socket_sendall_with_valid_requestline(POTENTIAL_HTTP_PORTS, httpretty, old_socket): ("fakesock.socket#sendall should create an entry if it's given a valid request line") - matcher = Mock() - info = Mock() + matcher = Mock(name='matcher') + info = Mock(name='info') httpretty.match_uriinfo.return_value = (matcher, info) httpretty.register_uri(httpretty.GET, 'http://foo.com/foobar') @@ -496,10 +496,10 @@ def test_fakesock_socket_sendall_with_valid_requestline(POTENTIAL_HTTP_PORTS, ht @patch('httpretty.core.old_socket') @patch('httpretty.core.httpretty') @patch('httpretty.core.POTENTIAL_HTTP_PORTS') -def test_fakesock_socket_sendall_with_valid_requestline(POTENTIAL_HTTP_PORTS, httpretty, old_socket): +def test_fakesock_socket_sendall_with_valid_requestline_2(POTENTIAL_HTTP_PORTS, httpretty, old_socket): ("fakesock.socket#sendall should create an entry if it's given a valid request line") - matcher = Mock() - info = Mock() + matcher = Mock(name='matcher') + info = Mock(name='info') httpretty.match_uriinfo.return_value = (matcher, info) httpretty.register_uri(httpretty.GET, 'http://foo.com/foobar') @@ -525,6 +525,7 @@ def test_fakesock_socket_sendall_with_body_data_no_entry(POTENTIAL_HTTP_PORTS, o ("fakesock.socket#sendall should call real_sendall when not parsing headers and there is no entry") # Background: # Using a subclass of socket that mocks out real_sendall + class MySocket(fakesock.socket): def real_sendall(self, data): data.should.equal(b'BLABLABLABLA') @@ -547,21 +548,17 @@ def test_fakesock_socket_sendall_with_body_data_no_entry(POTENTIAL_HTTP_PORTS, o @patch('httpretty.core.old_socket') @patch('httpretty.core.POTENTIAL_HTTP_PORTS') def test_fakesock_socket_sendall_with_body_data_with_entry(POTENTIAL_HTTP_PORTS, old_socket): - ("fakesock.socket#sendall should call real_sendall when not ") + ("fakesock.socket#sendall should call real_sendall when there is no entry") # Background: # Using a subclass of socket that mocks out real_sendall + data_sent = [] + class MySocket(fakesock.socket): def real_sendall(self, data): - raise AssertionError('should have never been called') - # Using a mocked entry - entry = Mock() - entry.request.headers = {} - entry.request.body = b'' + data_sent.append(data) # Given an instance of that socket socket = MySocket() - socket._entry = entry - # And that is is considered http socket.connect(('foo.com', 80)) @@ -569,21 +566,31 @@ def test_fakesock_socket_sendall_with_body_data_with_entry(POTENTIAL_HTTP_PORTS, # When I try to send data socket.sendall(b"BLABLABLABLA") - # Then the entry should have that body - entry.request.body.should.equal(b'BLABLABLABLA') + # Then it shoud have called real_sendall + data_sent.should.equal(['BLABLABLABLA']) +@patch('httpretty.core.httpretty.match_uriinfo') @patch('httpretty.core.old_socket') @patch('httpretty.core.POTENTIAL_HTTP_PORTS') -def test_fakesock_socket_sendall_with_body_data_with_chunked_entry(POTENTIAL_HTTP_PORTS, old_socket): +def test_fakesock_socket_sendall_with_body_data_with_chunked_entry(POTENTIAL_HTTP_PORTS, old_socket, match_uriinfo): ("fakesock.socket#sendall should call real_sendall when not ") # Background: # Using a subclass of socket that mocks out real_sendall + class MySocket(fakesock.socket): def real_sendall(self, data): raise AssertionError('should have never been called') + + matcher = Mock(name='matcher') + info = Mock(name='info') + httpretty.match_uriinfo.return_value = (matcher, info) + # Using a mocked entry entry = Mock() + entry.method = 'GET' + entry.info.path = '/foo' + entry.request.headers = { 'transfer-encoding': 'chunked', } diff --git a/tests/unit/test_httpretty.py b/tests/unit/test_httpretty.py index 4699e2b..fc32706 100644 --- a/tests/unit/test_httpretty.py +++ b/tests/unit/test_httpretty.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # -# Copyright (C) <2011-2013> Gabriel Falcão +# Copyright (C) <2011-2015> Gabriel Falcão # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -238,7 +238,9 @@ def test_uri_info_eq_ignores_case(): ) expect(uri_info_uppercase).to.equal(uri_info_lowercase) + def test_global_boolean_enabled(): + HTTPretty.disable() expect(HTTPretty.is_enabled()).to.be.falsy HTTPretty.enable() expect(HTTPretty.is_enabled()).to.be.truthy diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 95aa24e..6ce49d8 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -12,8 +12,11 @@ def test_last_request(original): httpretty.last_request().should.equal(original.last_request) + def test_has_request(): - """httpretty.has_request() correctly detects whether or not a request has been made""" + ("httpretty.has_request() correctly detects " + "whether or not a request has been made") + httpretty.reset() httpretty.has_request().should.be.false with patch('httpretty.httpretty.last_request', return_value=HTTPrettyRequest('')): httpretty.has_request().should.be.true