pax_global_header00006660000000000000000000000064143577647100014530gustar00rootroot0000000000000052 comment=cb4f783a9a7ec16f05b6bed442ee0202e19e257c pook-1.1.1/000077500000000000000000000000001435776471000125005ustar00rootroot00000000000000pook-1.1.1/.coveragerc000066400000000000000000000001401435776471000146140ustar00rootroot00000000000000[run] branch = True [report] exclude_lines = @abc.abstractmethod @abc.abstractproperty pook-1.1.1/.editorconfig000066400000000000000000000003561435776471000151610ustar00rootroot00000000000000root = true [*.{py,rst,txt}] indent_style = space indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true end_of_line = LF [*.yml] indent_style = space indent_size = 2 end_of_line = LF [Makefile] indent_style = tab pook-1.1.1/.gitignore000066400000000000000000000020451435776471000144710ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *.pyc *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject .DS_Store pook-1.1.1/.travis.yml000066400000000000000000000004111435776471000146050ustar00rootroot00000000000000language: python python: - 3.5 - 3.6 - 3.7 - 3.8 - 3.9 - 3.10 - pypy - pypy3 allow_failures: - python: pypy install: - pip install -r requirements.txt - pip install -r requirements-dev.txt script: - make test after_success: coveralls pook-1.1.1/History.rst000066400000000000000000000114671435776471000147040ustar00rootroot00000000000000 v1.1.0 / 2023-01-01 =================== * chore(version): bump minor v1.1.0 * Switch to Python >= 3.5 and fix latest aiohttp compatability (#83) * fix: remove print call (#81) v1.0.2 / 2021-09-10 =================== * fix(urllib3): interceptor is never really disabled (#68) * Closes #75 Re consider @fluent decorator (#76) * fix(#69): use match keyword in pytest.raises * fix(History): invalid rst syntax v1.0.1 / 2020-03-24 ------------------- * fix(aiohttp): compatible with non aiohttp projects (#67) * feat(History): add release changes v1.0.0 / 2020-03-18 ------------------- * fix(aiohttp): use latest version, allow Python 3.5+ for async http client v0.2.8 / 2019-10-31 ------------------- * fix collections import warning (#61) v0.2.7 / 2019-10-21 ------------------- * fix collections import warning (#61) v0.2.6 / 2019-02-01 ------------------- * Add mock.reply(new_response=True) to reset response definition object v0.2.5 / 2017-10-19 ------------------- * refactor(setup): remove extra install dependency * Fix py27 compatibility (#49) * Add activate_async decorator (#48) * fix typo in pook.mock.Mock.ismatched.__doc__ (#47) * fix README example (#46) v0.2.4 / 2017-10-03 ------------------- * fix(#45): regex URL issue * fix(travis): allow failures in pypy * feat(docs): add sponsor banner * refactor(History): normalize style v0.2.3 / 2017-04-28 ------------------- * feat(docs): add supported version for aiohttp * Merge branch 'master' of https://github.com/h2non/pook * fix(api): export missing symbol "disable_network" * Update README.rst (#43) v0.2.2 / 2017-04-03 ------------------- * refactor(compare): disable maxDiff length limit while comparing values v0.2.1 / 2017-03-25 ------------------- * fix(engine): enable new mock engine on register if needed * fix(engine): remove activate argument before instantiating the Mock v0.2.0 / 2017-03-18 ------------------- * refactor(engine): do not activate engine on mock declaration if not explicitly requested. This introduces a behavioral library change: you must explicitly use ``pook.on()`` to enable `pook` mock engine. v0.1.14 / 2017-03-17 -------------------- * feat(docs): list supported HTTP client versions * fix(#41): disable mocks after decorator call invokation * feat(examples): add mock context manager example file * feat(#40): support context manager definitions * feat(#39): improve unmatched request output * feat(docs): add mocket example file * feat(#33): add mocket examples and documentation v0.1.13 / 2017-01-29 -------------------- * fix(api): `mock.calls` property should be an `int`. v0.1.12 / 2017-01-28 -------------------- * feat(#33): proxy mock definitions into mock.Request * refactor(api): `pook.unmatched_requests()` now returns a `list` instead of a lazy `tuple`. v0.1.11 / 2017-01-14 -------------------- * refactor(query) * fix(#37): fix URL comparison * fix(#38): proper mock engine interface validation. v0.1.10 / 2017-01-13 -------------------- * fix(#37): decode byte bodies * feat(setup.py): add author email v0.1.9 / 2017-01-06 ------------------- * fix(Makefile): remove proper egg file * feat(package): add wheel package distribution support * feat(docs): add documentation links v0.1.8 / 2016-12-24 ------------------- * fix(assertion): extract regex pattern only when required * feat(examples): add regular expression example v0.1.7 / 2016-12-18 ------------------- * feat(#33): add support for user defined custom mock engine v0.1.6 / 2016-12-14 ------------------- * fix(setup.py): force utf-8 encoding * feat(setup.py): add encoding header * feat(api): add debug mode * refactor(docs): minor enhancements * refactor(tests): update URL matcher test cases * refactor(docs): add note about HTTP clients and update features list * fix(setup.py): remove encoding param * fix(tests): use strict equality assertion 0.1.5 / 2016-12-12 ------------------ * fix(matchers): fix matching issue in URL. * refactor(assertion): regex expression based matching must be explicitly enabled. * feat(tests): add initial matchers tests. 0.1.4 / 2016-12-08 ------------------ * refactor(README): minor changes * fix(setup.py): lint error * fix(#32): use explicit encoding while reading files in setup.py 0.1.3 / 2016-12-08 ------------------ * fix(core): several bug fixes. * feat(core): add pending features and major refactors. * feat(matchers): use ``unittest.TestCase`` matching engine by default. 0.1.2 / 2016-12-01 ------------------ * fix(matchers): runtime missing variable. 0.1.1 / 2016-12-01 ------------------ * fix: Python 2 dictionary iteration syntax. * feat(docs): add more examples. * fix(matchers): better regular expression comparison support. 0.1.0 / 2016-11-30 ------------------ * First version (still beta) 0.1.0-rc.1 / 2016-11-27 ----------------------- * First release candidate version (still beta) pook-1.1.1/LICENSE000066400000000000000000000020651435776471000135100ustar00rootroot00000000000000MIT License Copyright (c) 2016-2020 Tomás Aparicio 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. pook-1.1.1/MANIFEST.in000066400000000000000000000001611435776471000142340ustar00rootroot00000000000000include README.rst LICENSE History.rst include requirements.txt include requirements-dev.txt prune docs/_build pook-1.1.1/Makefile000066400000000000000000000027701435776471000141460ustar00rootroot00000000000000OK_COLOR=\033[32;01m NO_COLOR=\033[0m all: lint unit export PYTHONPATH:=${PWD} version=`python -c 'import pook; print(pook.__version__)'` filename=pook-`python -c 'import pook; print(pook.__version__)'`.tar.gz apidocs: @sphinx-apidoc -f --follow-links -H "API documentation" -o docs/source pook htmldocs: @rm -rf docs/_build $(MAKE) -C docs html install: @pip install -r requirements.txt @pip install -r requirements-dev.txt lint: @echo "$(OK_COLOR)==> Linting code ...$(NO_COLOR)" @flake8 . test: clean lint @echo "$(OK_COLOR)==> Runnings tests ...$(NO_COLOR)" @py.test -s -v --capture sys --cov pook --cov-report term-missing coverage: @coverage run --source pook -m py.test @coverage report tag: @echo "$(OK_COLOR)==> Creating tag $(version) ...$(NO_COLOR)" @git tag -a "v$(version)" -m "Version $(version)" @echo "$(OK_COLOR)==> Pushing tag $(version) to origin ...$(NO_COLOR)" @git push origin "v$(version)" bump: @bumpversion --commit --tag --current-version $(version) patch pook/__init__.py --allow-dirty bump-minor: @bumpversion --commit --tag --current-version $(version) minor pook/__init__.py --allow-dirty history: @git changelog --tag $(version) clean: @echo "$(OK_COLOR)==> Cleaning up files that are already in .gitignore...$(NO_COLOR)" @for pattern in `cat .gitignore`; do find . -name "*/$$pattern" -delete; done publish: @echo "$(OK_COLOR)==> Releasing package ...$(NO_COLOR)" @python setup.py sdist bdist_wheel @twine upload dist/* @rm -fr build dist .egg pook.egg-info pook-1.1.1/README.rst000066400000000000000000000212021435776471000141640ustar00rootroot00000000000000pook |Build Status| |PyPI| |Coverage Status| |Documentation Status| |Stability| |Quality| |Versions| ==================================================================================================== Versatile, expressive and hackable utility library for HTTP traffic mocking and expectations made easy in `Python`_. Heavily inspired by `gock`_. To get started, see the `documentation`_, `how it works`_, `FAQ`_ or `examples`_. Features -------- - Simple, expressive and fluent API. - Provides both Pythonic and chainable DSL API styles. - Full-featured HTTP response definitions and expectations. - Matches any HTTP protocol primitive (URL, method, query params, headers, body...). - Full regular expressions capable mock expectations matching. - Supports most popular HTTP clients via interceptor adapters. - Configurable volatile, persistent or TTL limited mocks. - Works with any testing framework/engine (unittest, pytest, nosetests...). - First-class JSON & XML support matching and responses. - Supports JSON Schema body matching. - Works in both runtime and testing environments. - Can be used as decorator and/or via context managers. - Supports real networking mode with optional traffic filtering. - Map/filter mocks easily for generic or custom mock expectations. - Custom user-defined mock matcher functions. - Simulated raised error exceptions. - Network delay simulation (only available for ``aiohttp``). - Pluggable and hackable API. - Customizable HTTP traffic mock interceptor engine. - Supports third-party mocking engines, such as `mocket`_. - Fits good for painless test doubles. - Does not support WebSocket traffic mocking. - Works with +3.5 (including PyPy). - Dependency-less: just 2 small dependencies for JSONSchema and XML tree comparison. Supported HTTP clients ---------------------- ``pook`` can work with multiple mock engines, however it provides a built-in one by default, which currently supports traffic mocking in the following HTTP clients: - ✔ `urllib3`_ v1+ - ✔ `requests`_ v2+ - ✔ `aiohttp`_ v1+ - v2+ - ✔ `urllib`_ / `http.client`_ v2/3 - ✘ `pycurl`_ (see `#16`_) More HTTP clients can be supported progressively. **Note**: only recent HTTP client package versions were tested. Installation ------------ Using ``pip`` package manager (requires pip 1.8+): .. code:: bash pip install --upgrade pook Or install the latest sources from Github: .. code:: bash pip install -e git+git://github.com/h2non/pook.git#egg=pook Getting started --------------- See ReadTheDocs documentation: |Documentation Status| API --- See `annotated API reference`_ documention. Examples -------- See `examples`_ documentation for full featured code and use case examples. Basic mocking: .. code:: python import pook import requests @pook.on def test_my_api(): mock = pook.get('http://twitter.com/api/1/foobar', reply=404, response_json={'error': 'not found'}) resp = requests.get('http://twitter.com/api/1/foobar') assert resp.status_code == 404 assert resp.json() == {"error": "not found"} assert mock.calls == 1 Using the chainable API DSL: .. code:: python import pook import requests @pook.on def test_my_api(): mock = (pook.get('http://twitter.com/api/1/foobar') .reply(404) .json({'error': 'not found'})) resp = requests.get('http://twitter.com/api/1/foobar') assert resp.json() == {"error": "not found"} assert mock.calls == 1 Using the decorator: .. code:: python import pook import requests @pook.get('http://httpbin.org/status/500', reply=204) @pook.get('http://httpbin.org/status/400', reply=200) def fetch(url): return requests.get(url) res = fetch('http://httpbin.org/status/400') print('#1 status:', res.status_code) res = fetch('http://httpbin.org/status/500') print('#2 status:', res.status_code) Simple ``unittest`` integration: .. code:: python import pook import unittest import requests class TestUnitTestEngine(unittest.TestCase): @pook.on def test_request(self): pook.get('server.com/foo').reply(204) res = requests.get('http://server.com/foo') self.assertEqual(res.status_code, 204) def test_request_with_context_manager(self): with pook.use(): pook.get('server.com/bar', reply=204) res = requests.get('http://server.com/bar') self.assertEqual(res.status_code, 204) Using the context manager for isolated HTTP traffic interception blocks: .. code:: python import pook import requests # Enable HTTP traffic interceptor with pook.use(): pook.get('http://httpbin.org/status/500', reply=204) res = requests.get('http://httpbin.org/status/500') print('#1 status:', res.status_code) # Interception-free HTTP traffic res = requests.get('http://httpbin.org/status/200') print('#2 status:', res.status_code) Example using `mocket`_ Python library as underlying mock engine: .. code:: python import pook import requests from mocket.plugins.pook_mock_engine import MocketEngine # Use mocket library as underlying mock engine pook.set_mock_engine(MocketEngine) # Explicitly enable pook HTTP mocking (optional) pook.on() # Target server URL to mock out url = 'http://twitter.com/api/1/foobar' # Define your mock mock = pook.get(url, reply=404, times=2, headers={'content-type': 'application/json'}, response_json={'error': 'foo'}) # Run first HTTP request requests.get(url) assert mock.calls == 1 # Run second HTTP request res = requests.get(url) assert mock.calls == 2 # Assert response data assert res.status_code == 404 assert res.json() == {'error': 'foo'} # Explicitly disable pook (optional) pook.off() Example using Hy language (Lisp dialect for Python): .. code:: hy (import [pook]) (import [requests]) (defn request [url &optional [status 404]] (doto (.mock pook url) (.reply status)) (let [res (.get requests url)] (. res status_code))) (defn run [] (with [(.use pook)] (print "Status:" (request "http://server.com/foo" :status 204)))) ;; Run test program (defmain [&args] (run)) Development ----------- Clone the repository: .. code:: bash git clone git@github.com:h2non/pook.git Install dependencies: .. code:: bash pip install -r requirements.txt -r requirements-dev.txt Install Python dependencies: .. code:: bash make install Lint code: .. code:: bash make lint Run tests: .. code:: bash make test Generate documentation: .. code:: bash make htmldocs License ------- MIT - Tomas Aparicio .. _Go: https://golang.org .. _Python: http://python.org .. _gock: https://github.com/h2non/gock .. _annotated API reference: http://pook.readthedocs.io/en/latest/api.html .. _#16: https://github.com/h2non/pook/issues/16 .. _examples: http://pook.readthedocs.io/en/latest/examples.html .. _aiohttp: https://github.com/KeepSafe/aiohttp .. _requests: http://docs.python-requests.org/en/master/ .. _urllib3: https://github.com/shazow/urllib3 .. _urllib: https://docs.python.org/3/library/urllib.html .. _http.client: https://docs.python.org/3/library/http.client.html .. _pycurl: http://pycurl.io .. _documentation: http://pook.readthedocs.io/en/latest/ .. _FAQ: http://pook.readthedocs.io/en/latest/faq.html .. _how it works: http://pook.readthedocs.io/en/latest/how_it_works.html .. _mocket: https://github.com/mindflayer/python-mocket .. |Build Status| image:: https://travis-ci.org/h2non/pook.svg?branch=master :target: https://travis-ci.org/h2non/pook .. |PyPI| image:: https://img.shields.io/pypi/v/pook.svg?maxAge=2592000?style=flat-square :target: https://pypi.python.org/pypi/pook .. |Coverage Status| image:: https://coveralls.io/repos/github/h2non/pook/badge.svg?branch=master :target: https://coveralls.io/github/h2non/pook?branch=master .. |Documentation Status| image:: https://img.shields.io/badge/docs-latest-green.svg?style=flat :target: http://pook.readthedocs.io/en/latest/?badge=latest .. |Quality| image:: https://codeclimate.com/github/h2non/pook/badges/gpa.svg :target: https://codeclimate.com/github/h2non/pook :alt: Code Climate .. |Stability| image:: https://img.shields.io/pypi/status/pook.svg :target: https://pypi.python.org/pypi/pook :alt: Stability .. |Versions| image:: https://img.shields.io/pypi/pyversions/pook.svg :target: https://pypi.python.org/pypi/pook :alt: Python Versions pook-1.1.1/docs/000077500000000000000000000000001435776471000134305ustar00rootroot00000000000000pook-1.1.1/docs/Makefile000066400000000000000000000166621435776471000151030ustar00rootroot00000000000000# 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) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help 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 " epub3 to make an epub3" @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)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp 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." .PHONY: qthelp 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/pook`.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pook`.qhc" .PHONY: applehelp 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." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/pook`" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pook`" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex 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)." .PHONY: latexpdf 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." .PHONY: latexpdfja 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." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo 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)." .PHONY: info 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." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck 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." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." pook-1.1.1/docs/api.rst000066400000000000000000000121401435776471000147310ustar00rootroot00000000000000.. _api: API Documentation ================= Core API -------- .. toctree:: pook.activate pook.on pook.disable pook.off pook.reset pook.engine pook.use pook.context pook.enable_network pook.disable_network pook.use_network pook.use_network_filter pook.flush_network_filters pook.mock pook.get pook.put pook.delete pook.head pook.patch pook.options pook.pending pook.ispending pook.pending_mocks pook.unmatched_requests pook.unmatched pook.isunmatched pook.isactive pook.isdone pook.regex pook.Mock pook.Engine pook.Request pook.Response pook.MockEngine pook.MatcherEngine .. automodule:: pook :members: :undoc-members: :show-inheritance: Matchers API ------------ .. toctree:: pook.matchers.init pook.matchers.add pook.matchers.get pook.matchers.BaseMatcher pook.matchers.MethodMatcher pook.matchers.URLMatcher pook.matchers.HeadersMatcher pook.matchers.PathMatcher pook.matchers.BodyMatcher pook.matchers.XMLMatcher pook.matchers.JSONMatcher pook.matchers.JSONSchemaMatcher pook.matchers.QueryMatcher .. automodule:: pook.matchers :members: :undoc-members: :show-inheritance: Interceptors API ---------------- .. toctree:: pook.interceptors.add pook.interceptors.get pook.interceptors.BaseInterceptor pook.interceptors.Urllib3Interceptor pook.interceptors.AIOHTTPInterceptor pook.interceptors.HTTPClientInterceptor .. automodule:: pook.interceptors :members: :undoc-members: :show-inheritance: pook-1.1.1/docs/conf.py000066400000000000000000000237521435776471000147400ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # pook documentation build configuration file, created by # sphinx-quickstart on Tue Oct 4 18:59:54 2016. # # 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. # 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. # import os import sys sys.path.insert(0, os.path.abspath('..')) import pook # noqa import sphinx_rtd_theme # noqa # -- 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.viewcode', 'sphinx.ext.autosummary', 'sphinx.ext.napoleon', ] # 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 = 'pook' copyright = '2016, Tomas Aparicio' author = 'Tomas Aparicio' # 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 = pook.__version__ # The full version, including alpha/beta/rc tags. release = pook.__version__ # 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. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # 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 = False # -- 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 = 'alabaster' 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. # " v documentation" by default. # # html_title = 'pook v0.1.0' # 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 (relative to this directory) to use as a 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 None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = None # 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', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' # # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. # # 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 = 'pookdoc' # -- 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, 'pook.tex', 'pook Documentation', 'Tomas Aparicio', '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 = [] # It false, will not define \strong, \code, itleref, \crossref ... but only # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added # packages. # # latex_keep_old_macro_names = True # 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, 'pook', 'pook 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, 'pook', 'pook Documentation', author, 'pook', '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 # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'http://docs.python.org/': None} pook-1.1.1/docs/examples.rst000066400000000000000000000056411435776471000160060ustar00rootroot00000000000000Examples ======== Basic mocking example using requests ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/requests_client.py Chainable API DSL ^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/chainable_api.py Context manager for isolated mocking ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/context_manager.py Single mock context manager definition for isolated mocking ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/mock_context_manager.py Declaring mocks as decorators ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/decorators.py Activating the mock engine via decorator within the function context ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/decorator_activate.py Activating the mock engine via decorator within an async coroutine function context ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/decorator_activate_async.py Featured JSON body matching ^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/json_matching.py JSONSchema based body matching ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/json_schema.py Enable real networking mode ^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/network_mode.py Persistent mock ^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/persistent_mock.py Time TTL limited mock ^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/time_ttl_mock.py Regular expression matching ^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/regex.py ``unittest`` integration ^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/unittest_example.py ``py.test`` integration ^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/pytest_example.py Simulated error exception on mock matching ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/simulated_error.py Using ``urllib3`` as HTTP client ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/urllib3_client.py Using ``urllib3`` to return a chunked response ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/urllib3_chunked_response.py Asynchronous HTTP request using ``aiohttp`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/aiohttp_client.py Using ``http.client`` standard Python package as HTTP client ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/http_client_native.py Example using `mocket`_ Python library as underlying mock engine ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/mocket_example.py Hy programming language example ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. literalinclude:: ../examples/basic.hy .. _mocket: https://github.com/mindflayer/python-mocket pook-1.1.1/docs/faq.rst000066400000000000000000000041241435776471000147320ustar00rootroot00000000000000FAQ === How does it work? ----------------- Please, read `how it works`_ section. What HTTP clients are supported? -------------------------------- Please, see `supported HTTP clients`_ section. .. _supported HTTP clients: index.html#supported-http-clients .. _how it works: how_it_works.html Does ``pook`` mock out all the outgoing HTTP traffic from my app? ----------------------------------------------------------------- Yes, that's the default behaviour: any outgoing HTTP traffic across the supported HTTP clients will be intercepted by ``pook``. In case that an outgoing traffic does not match any mock expectation, an exception error will be raised, telling you no mock was matched in order to review or fix your code accordingly. You can for sure change this behaviour and don't raise any exception if no mock definition can be matched. You can change this enabling the real networking mode via ``pook.enable_network()``. Can I use ``pook`` in a non testing environment? ------------------------------------------------ Absolutely. ``pook`` is testing environment agnostic. You simply have to take care of the side effects of mocking HTTP traffic in a runtime environment. For that cases you probably want to enable the real networking mode. Can I use ``pook`` with a custom HTTP traffic mock interceptor engine? ---------------------------------------------------------------------- Yes, you can. ``pook`` is very modular and open for extensibility. You can programmatically define the HTTP traffic mock engine you want to use via ``pook.set_mock_engine(engine)``. This will replace the built-in one. This can be particularly useful if you are already using another HTTP mocking engine that satisfy your needs, but you want to take benefit of ``pook`` features, versatility and simple to use expressive API. For mock engine implementation details, see ``pook.MockEngine`` API documentation. Can I use ``pook`` with any test framework? ------------------------------------------- Yes. ``pook`` is test framework agnostic. You can use it within ``unittest``, ``nosetests``, ``pytest`` or others. pook-1.1.1/docs/history.rst000066400000000000000000000000341435776471000156600ustar00rootroot00000000000000.. include:: ../History.rst pook-1.1.1/docs/how_it_works.rst000066400000000000000000000051101435776471000166750ustar00rootroot00000000000000How it works ============ HTTP traffic interception ------------------------- In a nutshell, ``pook`` uses ``unittest.mock`` standard Python package in order to patch external library objects, allowing ``pook`` HTTP interceptor adapter to patch any third-party packages and intercept specific function calls. ``pook`` entirely relies on this interception strategy, therefore in the meantime ``pook`` is active, any outgoing HTTP traffic intercepted by the supported HTTP clients won't imply any real TCP networking, except if you enabled the real networking mode via ``pook.enable_network()``, which in that case real network traffic can be established. Worth clarifying that ``pook`` only works at Python programmatic runtime level. There's no network/socket programming involved. HTTP request matching --------------------- Outgoing HTTP traffic is intercepted and matched against a pool of mock matchers defined in your mock expectations. Matching process in sequential and executed as FIFO order, meaning the first has always preference. For instance, if you declare multiple identical mocks, the first one will be matched first and the others will be ignored. Once the first one expires, the subsequent mock definition in the chain will be matched instead. Real networking mode -------------------- By default real networking mode is disabled. This basically means that real networking will not happen unless you explicitely enable it. This behaviour has been adopted to improve predictability, control and mitigate developers fear between behaviour boundaries and expectations. ``pook`` will prevent you to communicate with real HTTP servers unless you enable it via: ``pook.enable_network()``. Also, you can partially restrict the real networking by filtering only certain hosts. You can do that via ``pook.use_network_filter(filter_fn)``. Volatile vs Persistent mocks ---------------------------- By default, mocks are volatile. This means that once a mock has been matched, and therefore consumed, it will be flushed. You can modify this behaviour via: Explicitly definining the TTL of each mock, so effectively the number of times the mock can be matched and consumed: .. code:: python # Match a mock up to 5 times, then flush it pook.get('server.com/api').times(5) # The above is equivalent to pook.get('server.com/api', times=5) Explicitly definining a persistent mock: .. code:: python # Make a mock definition persistent, so it won't be never flushed pook.get('server.com/api').persist() # The above is equivalent to pook.get('server.com/api', persist=True) pook-1.1.1/docs/index.rst000066400000000000000000000007421435776471000152740ustar00rootroot00000000000000Contents -------- .. toctree:: :maxdepth: 2 Features Supported HTTP clients install how_it_works examples api faq Development history .. include:: ../README.rst Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` pook-1.1.1/docs/install.rst000066400000000000000000000007521435776471000156340ustar00rootroot00000000000000Installation ============ PyPI ---- You can install the last stable release of Expects from PyPI using pip or easy_install:: $ pip install pook GitHub ------ Or install the latest sources from Github:: $ pip install -e git+git://github.com/h2non/pook.git#egg=pook Also you can download a source code package from `Github `_ and install it using setuptools:: $ tar xvf pook-{version}.tar.gz $ cd pook $ python setup.py install pook-1.1.1/docs/make.bat000066400000000000000000000170601435776471000150410ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :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. epub3 to make an epub3 echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over 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 echo. dummy to check syntax errors of document sources goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 1>NUL 2>NUL if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pook.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pook.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "epub3" ( %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) if "%1" == "dummy" ( %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy if errorlevel 1 exit /b 1 echo. echo.Build finished. Dummy builder generates no files. goto end ) :end pook-1.1.1/examples/000077500000000000000000000000001435776471000143165ustar00rootroot00000000000000pook-1.1.1/examples/README.rst000066400000000000000000000004161435776471000160060ustar00rootroot00000000000000``pook`` examples ================= Run examples via: .. code:: python $ python examples/.py Example: .. code:: python $ python examples/requests_client.py Contributing ------------ Pull requests are very welcome adding more useful examples. pook-1.1.1/examples/aiohttp_client.py000066400000000000000000000015041435776471000176760ustar00rootroot00000000000000# flake8: noqa import pook import aiohttp import asyncio import async_timeout async def fetch(session, url, data): with async_timeout.timeout(10): async with session.get(url, data=data) as res: print('Status:', res.status) print('Headers:', res.headers) print('Body:', await res.text()) with pook.use(network=True): pook.get('http://httpbin.org/ip', reply=404, response_type='json', response_headers={'Server': 'nginx'}, response_json={'error': 'not found'}) async def main(loop): async with aiohttp.ClientSession(loop=loop) as session: await fetch(session, 'http://httpbin.org/ip', bytearray('foo bar', 'utf-8')) loop = asyncio.get_event_loop() loop.run_until_complete(main(loop)) pook-1.1.1/examples/basic.hy000066400000000000000000000005111435776471000157360ustar00rootroot00000000000000(import [pook]) (import [requests]) (defn request [url &optional [status 404]] (doto (.mock pook url) (.reply status)) (let [res (.get requests url)] (. res status_code))) (defn run [] (with [(.use pook)] (print "Status:" (request "http://foo.com/bar" :status 204)))) ;; Run test program (defmain [&args] (run)) pook-1.1.1/examples/chainable_api.py000066400000000000000000000011631435776471000174300ustar00rootroot00000000000000import json import pook import requests # Enable mock engine pook.on() (pook.post('httpbin.org/post') .json({'foo': 'bar'}) .type('json') .header('Client', 'requests') .reply(204) .headers({'server': 'pook'}) .json({'error': 'simulated'})) res = requests.post('http://httpbin.org/post', data=json.dumps({'foo': 'bar'}), headers={'Client': 'requests', 'Content-Type': 'application/json'}) print('Status:', res.status_code) print('Body:', res.json()) print('Is done:', pook.isdone()) print('Pending mocks:', pook.pending_mocks()) pook-1.1.1/examples/context_manager.py000066400000000000000000000007561435776471000200560ustar00rootroot00000000000000import pook import requests with pook.context(): pook.get('httpbin.org/ip', reply=403, response_headers={'pepe': 'lopez'}, response_json={'error': 'not found'}) res = requests.get('http://httpbin.org/ip') print('Status:', res.status_code) print('Headers:', res.headers) print('Body:', res.json()) print('Is done:', pook.isdone()) print('Pending mocks:', pook.pending_mocks()) print('Unmatched requests:', pook.unmatched_requests()) pook-1.1.1/examples/decorator_activate.py000066400000000000000000000007651435776471000205420ustar00rootroot00000000000000import pook import requests @pook.on def run(): pook.get('httpbin.org/ip', reply=403, response_headers={'pepe': 'lopez'}, response_json={'error': 'not found'}) res = requests.get('http://httpbin.org/ip') print('Status:', res.status_code) print('Headers:', res.headers) print('Body:', res.json()) print('Is done:', pook.isdone()) print('Pending mocks:', pook.pending_mocks()) print('Unmatched requests:', pook.unmatched_requests()) run() pook-1.1.1/examples/decorator_activate_async.py000066400000000000000000000013571435776471000217350ustar00rootroot00000000000000# flake8: noqa import pook import asyncio import aiohttp # # NOTE: requires Python 3.5+ # @pook.on async def run(): pook.get('httpbin.org/ip', reply=403, response_headers={'pepe': 'lopez'}, response_json={'error': 'not found'}) async with aiohttp.ClientSession(loop=loop) as session: async with session.get('http://httpbin.org/ip') as res: print('Status:', res.status) print('Headers:', res.headers) print('Body:', await res.text()) print('Is done:', pook.isdone()) print('Pending mocks:', pook.pending_mocks()) print('Unmatched requests:', pook.unmatched_requests()) loop = asyncio.get_event_loop() loop.run_until_complete(run()) pook-1.1.1/examples/decorators.py000066400000000000000000000011361435776471000170360ustar00rootroot00000000000000import pook import requests @pook.get('http://httpbin.org/status/500', reply=204) @pook.get('http://httpbin.org/status/400', reply=200, persist=True) def fetch(url): return requests.get(url) # Test function res = fetch('http://httpbin.org/status/400') print('#1 status:', res.status_code) res = fetch('http://httpbin.org/status/500') print('#2 status:', res.status_code) print('Is done:', pook.isdone()) print('Pending mocks:', pook.pending_mocks()) # Disable mock engine pook.off() # Test real request res = requests.get('http://httpbin.org/status/400') print('Test status:', res.status_code) pook-1.1.1/examples/http_client_native.py000066400000000000000000000011041435776471000205470ustar00rootroot00000000000000import http.client import pook # Enable mock engine pook.on() mock = pook.get('http://httpbin.org/ip', reply=404, response_type='json', response_json={'error': 'not found'}) conn = http.client.HTTPConnection('httpbin.org') conn.request('GET', '/ip') res = conn.getresponse() print('Status:', res.status, res.reason) print('Headers:', res.headers) print('Body:', res.read()) print('Mock calls:', mock.calls) print('Is done:', pook.isdone()) print('Pending mocks:', pook.pending_mocks()) print('Unmatched requests:', pook.unmatched_requests()) pook-1.1.1/examples/json_matching.py000066400000000000000000000006471435776471000175220ustar00rootroot00000000000000import json import pook import requests # Enable mock engine pook.on() (pook.post('httpbin.org/post') .json({'foo': 'bar'}) .reply(204) .json({'error': 'simulated'})) res = requests.post('http://httpbin.org/post', data=json.dumps({'foo': 'bar'})) print('Status:', res.status_code) print('Body:', res.json()) print('Is done:', pook.isdone()) print('Pending mocks:', pook.pending_mocks()) pook-1.1.1/examples/json_schema.py000066400000000000000000000010101435776471000171510ustar00rootroot00000000000000import json import pook import requests schema = { 'type': 'object', 'properties': { 'foo': {'type': 'string'}, } } # Enable mock engine pook.on() (pook.post('httpbin.org/post') .jsonschema(schema) .reply(204) .json({'error': 'simulated'})) res = requests.post('http://httpbin.org/post', data=json.dumps({'foo': 'bar'})) print('Status:', res.status_code) print('Body:', res.json()) print('Is done:', pook.isdone()) print('Pending mocks:', pook.pending_mocks()) pook-1.1.1/examples/match_callback.py000066400000000000000000000011211435776471000175730ustar00rootroot00000000000000import pook import requests def on_match(request, mock): print('On match:', request, mock) # Enable mock engine pook.on() pook.get('httpbin.org/ip', reply=403, response_type='json', response_headers={'pepe': 'lopez'}, response_json={'error': 'not found'}, callback=on_match) res = requests.get('http://httpbin.org/ip') print('Status:', res.status_code) print('Headers:', res.headers) print('Body:', res.json()) print('Is done:', pook.isdone()) print('Pending mocks:', pook.pending_mocks()) print('Unmatched requests:', pook.unmatched_requests()) pook-1.1.1/examples/mock_context_manager.py000066400000000000000000000022161435776471000210600ustar00rootroot00000000000000import pook import requests # Define a new mock that will be only active within the context manager with pook.get('httpbin.org/ip', reply=403, response_headers={'pepe': 'lopez'}, response_json={'error': 'not found'}) as mock: res = requests.get('http://httpbin.org/ip') print('#1 Status:', res.status_code) print('#1 Headers:', res.headers) print('#1 Body:', res.json()) print('----------------') res = requests.get('http://httpbin.org/ip') print('#2 Status:', res.status_code) print('#2 Headers:', res.headers) print('#2 Body:', res.json()) print('----------------') print('Mock is done:', mock.isdone()) print('Mock matches:', mock.total_matches) print('Is done:', pook.isdone()) print('Pending mocks:', pook.pending_mocks()) print('Unmatched requests:', pook.unmatched_requests()) # Explicitly disable mock engine pook.off() # Perform a real HTTP request since we are running the # request outside of the context manager res = requests.get('http://httpbin.org/ip') print('#3 Status:', res.status_code) print('#3 Headers:', res.headers) print('#3 Body:', res.json()) pook-1.1.1/examples/mocket_example.py000066400000000000000000000015011435776471000176620ustar00rootroot00000000000000# Be sure you have `mocket` installed: # $ pip install mocket import pook import requests from mocket.plugins.pook_mock_engine import MocketEngine # Use mocket library as underlying mock engine pook.set_mock_engine(MocketEngine) # Explicitly enable pook HTTP mocking (required) pook.on() # Target server URL to mock out url = 'http://twitter.com/api/1/foobar' # Define your mock mock = pook.get(url, reply=404, times=2, headers={'content-type': 'application/json'}, response_json={'error': 'foo'}) # Run first HTTP request requests.get(url) assert mock.calls == 1 # Run second HTTP request res = requests.get(url) assert mock.calls == 2 # Assert response data assert res.status_code == 404 assert res.json() == {'error': 'foo'} # Explicitly disable pook (optional) pook.off() pook-1.1.1/examples/network_mode.py000066400000000000000000000011251435776471000173640ustar00rootroot00000000000000import pook import requests # Enable mock engine pook.on() # Enable network mode pook.enable_network() (pook.get('httpbin.org/headers') .reply(204) .headers({'server': 'pook'}) .json({'error': 'simulated'})) res = requests.get('http://httpbin.org/headers') print('Mock status:', res.status_code) # Real network request, since pook cannot match any mock res = requests.get('http://httpbin.org/ip') print('Real status:', res.status_code) print('Is done:', pook.isdone()) print('Pending mocks:', pook.pending_mocks()) # Disable network mode once we're done pook.disable_network() pook-1.1.1/examples/nose_example.py000066400000000000000000000016661435776471000173600ustar00rootroot00000000000000# -*- coding: utf-8 -*- import pook import requests @pook.activate def test_simple_pook_request(): pook.get('server.com/foo').reply(204) res = requests.get('http://server.com/foo') assert res.status_code == 204 @pook.on def test_enable_engine(): pook.get('server.com/foo').reply(204) res = requests.get('http://server.com/foo') assert res.status_code == 204 @pook.get('server.com/foo', reply=204) def test_decorator(): res = requests.get('http://server.com/foo') assert res.status_code == 204 def test_context_manager(): with pook.use(): pook.get('server.com/bar', reply=204) res = requests.get('http://server.com/bar') assert res.status_code == 204 @pook.on def test_no_match_exception(): pook.get('server.com/bar', reply=204) try: requests.get('http://server.com/baz') except Exception: pass else: raise RuntimeError('expected to fail') pook-1.1.1/examples/persistent_mock.py000066400000000000000000000007511435776471000201040ustar00rootroot00000000000000import pook import requests # Enable mock engine pook.on() (pook.get('httpbin.org') .headers({'Client': 'requests'}) .persist() .reply(400) .headers({'server': 'pook'}) .json({'error': 'simulated'})) res = requests.get('http://httpbin.org', headers={'Client': 'requests'}) print('Status:', res.status_code) print('Headers:', res.headers) print('Body:', res.json()) print('Is done:', pook.isdone()) print('Pending mocks:', pook.pending_mocks()) pook-1.1.1/examples/pytest_example.py000066400000000000000000000016251435776471000177370ustar00rootroot00000000000000# -*- coding: utf-8 -*- import pook import pytest import requests @pook.activate def test_simple_pook_request(): pook.get('server.com/foo').reply(204) res = requests.get('http://server.com/foo') assert res.status_code == 204 @pook.on def test_enable_engine(): pook.get('server.com/foo').reply(204) res = requests.get('http://server.com/foo') assert res.status_code == 204 pook.disable() @pook.get('server.com/bar', reply=204) def test_decorator(): res = requests.get('http://server.com/bar') assert res.status_code == 204 def test_context_manager(): with pook.use(): pook.get('server.com/baz', reply=204) res = requests.get('http://server.com/baz') assert res.status_code == 204 @pook.on def test_no_match_exception(): pook.get('server.com/bar', reply=204) with pytest.raises(Exception): requests.get('http://server.com/baz') pook-1.1.1/examples/regex.py000066400000000000000000000013661435776471000160100ustar00rootroot00000000000000import pook import requests # Enable mock engine pook.on() # Mock definition based (pook.get(pook.regex('h[t]{2}pbin.*')) .path(pook.regex('/foo/[a-z]+/baz')) .header('Client', pook.regex('requests|pook')) .times(2) .reply(200) .headers({'server': 'pook'}) .json({'foo': 'bar'})) # Perform request res = requests.get('http://httpbin.org/foo/bar/baz', headers={'Client': 'requests'}) print('Status:', res.status_code) print('Body:', res.json()) # Perform second request res = requests.get('http://httpbin.org/foo/foo/baz', headers={'Client': 'pook'}) print('Status:', res.status_code) print('Body:', res.json()) print('Is done:', pook.isdone()) print('Pending mocks:', pook.pending_mocks()) pook-1.1.1/examples/requests_client.py000066400000000000000000000014471435776471000201070ustar00rootroot00000000000000import pook import requests # Enable mock engine pook.on() pook.get('httpbin.org/ip', reply=403, response_type='json', response_headers={'server': 'pook'}, response_json={'error': 'not found'}) pook.get('httpbin.org/headers', reply=404, response_type='json', response_headers={'server': 'pook'}, response_json={'error': 'not found'}) res = requests.get('http://httpbin.org/ip') print('Status:', res.status_code) print('Headers:', res.headers) print('Body:', res.json()) res = requests.get('http://httpbin.org/headers') print('Status:', res.status_code) print('Headers:', res.headers) print('Body:', res.json()) print('Is done:', pook.isdone()) print('Pending mocks:', pook.pending_mocks()) print('Unmatched requests:', pook.unmatched_requests()) pook-1.1.1/examples/simulated_error.py000066400000000000000000000004371435776471000200740ustar00rootroot00000000000000import pook import requests # Enable mock engine pook.on() # Simulated error exception on request matching pook.get('httpbin.org/status/500', error=Exception('simulated error')) try: requests.get('http://httpbin.org/status/500') except Exception as err: print('Error:', err) pook-1.1.1/examples/time_ttl_mock.py000066400000000000000000000012341435776471000175220ustar00rootroot00000000000000import pook import requests # Enable mock engine pook.on() # Declare mock (pook.get('httpbin.org') .times(2) .reply(400) .headers({'server': 'pook'}) .json({'error': 'simulated'})) # Mock request 1 res = requests.get('http://httpbin.org') print('#1 status:', res.status_code) print('#1 body:', res.json()) # Mock request 2 res = requests.get('http://httpbin.org') print('#2 status:', res.status_code) print('#2 body:', res.json()) # Real request 3 try: requests.get('http://httpbin.org') except Exception: print('Request #3 not matched due to expired mock') print('Is done:', pook.isdone()) print('Pending mocks:', pook.pending_mocks()) pook-1.1.1/examples/unittest_example.py000066400000000000000000000014141435776471000202620ustar00rootroot00000000000000import pook import unittest import requests class TestUnitTestEngine(unittest.TestCase): @pook.on def test_request(self): pook.get('server.com/foo').reply(204) res = requests.get('http://server.com/foo') self.assertEqual(res.status_code, 204) def test_request_with_context_manager(self): with pook.use(): pook.get('server.com/bar', reply=204) res = requests.get('http://server.com/bar') self.assertEqual(res.status_code, 204) @pook.on def test_no_match_exception(self): pook.get('server.com/bar', reply=204) try: requests.get('http://server.com/baz') except Exception: pass else: raise RuntimeError('expected to fail') pook-1.1.1/examples/urllib3_chunked_response.py000066400000000000000000000005501435776471000216630ustar00rootroot00000000000000import pook import urllib3 # Mock HTTP traffic only in the given context with pook.use(): (pook.get('httpbin.org/chunky') .reply(200) .body(['returned', 'as', 'chunks'], chunked=True)) # Intercept request http = urllib3.PoolManager() r = http.request('GET', 'httpbin.org/chunky') print('Chunks:', list(r.read_chunked())) pook-1.1.1/examples/urllib3_client.py000066400000000000000000000007221435776471000176030ustar00rootroot00000000000000import pook import urllib3 # Mock HTTP traffic only in the given context with pook.use(): pook.get('http://httpbin.org/status/404').reply(204) # Intercept request http = urllib3.PoolManager() r = http.request('GET', 'http://httpbin.org/status/404') print('#1 status:', r.status) # Real request outside of the context manager http = urllib3.PoolManager() r = http.request('GET', 'http://httpbin.org/status/404') print('#2 status:', r.status) pook-1.1.1/pook/000077500000000000000000000000001435776471000134505ustar00rootroot00000000000000pook-1.1.1/pook/__init__.py000066400000000000000000000003421435776471000155600ustar00rootroot00000000000000from .api import * # noqa from .api import __all__ as api_exports # Delegate to API export __all__ = api_exports # Package metadata __author__ = 'Tomas Aparicio' __license__ = 'MIT' # Current version __version__ = '1.1.1' pook-1.1.1/pook/activate_async.py000066400000000000000000000012331435776471000170160ustar00rootroot00000000000000import functools from asyncio import iscoroutinefunction def activate_async(fn, _engine): """ Async version of activate decorator Arguments: fn (function): function that be wrapped by decorator. _engine (Engine): pook engine instance Returns: function: decorator wrapper function. """ @functools.wraps(fn) async def wrapper(*args, **kw): _engine.activate() try: if iscoroutinefunction(fn): async for v in fn(*args, **kw): yield v else: fn(*args, **kw) finally: _engine.disable() return wrapper pook-1.1.1/pook/api.py000066400000000000000000000303131435776471000145730ustar00rootroot00000000000000import functools import re from contextlib import contextmanager from inspect import isfunction from .engine import Engine from .matcher import MatcherEngine from .mock import Mock from .mock_engine import MockEngine from .request import Request from .response import Response try: from asyncio import iscoroutinefunction except ImportError: iscoroutinefunction = None if iscoroutinefunction is not None: from .activate_async import activate_async else: activate_async = None # Public API symbols to export __all__ = ( 'activate', 'on', 'disable', 'off', 'reset', 'engine', 'use_network', 'enable_network', 'disable_network', 'get', 'post', 'put', 'patch', 'head', 'use', 'set_mock_engine', 'delete', 'options', 'pending', 'ispending', 'mock', 'pending_mocks', 'unmatched_requests', 'isunmatched', 'unmatched', 'isactive', 'isdone', 'regex', 'Engine', 'Mock', 'Request', 'Response', 'MatcherEngine', 'MockEngine', 'use_network_filter' ) # Default singleton mock engine to be used _engine = Engine() def debug(enable=True): """ Enables or disables debug mode in the current mock engine. Arguments: enable (bool): ``True`` to enable debug mode. Otherwise ``False``. """ _engine.debug = enable def engine(): """ Returns the current running mock engine. Returns: pook.Engine: current used engine. """ return _engine def set_mock_engine(engine): """ Sets a custom mock engine, replacing the built-in one. This is particularly useful if you want to replace the built-in HTTP traffic mock interceptor engine with your custom one. For mock engine implementation details, see `pook.MockEngine`. Arguments: engine (pook.MockEngine): custom mock engine to use. """ _engine.set_mock_engine(engine) def activate(fn=None): """ Enables the HTTP traffic interceptors. This function can be used as decorator. Arguments: fn (function|coroutinefunction): Optional function argument if used as decorator. Returns: function: decorator wrapper function, only if called as decorator, otherwise ``None``. Example:: # Standard use case pook.activate() pook.mock('server.com/foo').reply(404) res = requests.get('server.com/foo') assert res.status_code == 404 pook.disable() # Decorator use case @pook.activate def test_request(): pook.mock('server.com/foo').reply(404) res = requests.get('server.com/foo') assert res.status_code == 404 """ # If not used as decorator, activate the engine and exit if not isfunction(fn): _engine.activate() return None # If used as decorator for an async coroutine, wrap it if iscoroutinefunction is not None and iscoroutinefunction(fn): return activate_async(fn, _engine) @functools.wraps(fn) def wrapper(*args, **kw): _engine.activate() try: fn(*args, **kw) finally: _engine.disable() return wrapper def on(fn=None): """ Enables the HTTP traffic interceptors. Alias to ``pook.activate()``. Arguments: fn (function|coroutinefunction): Optional function argument if used as decorator. Returns: function: decorator wrapper function, only if called as decorator. Example:: # Standard usage pook.on() pook.mock('server.com/foo').reply(404) res = requests.get('server.com/foo') assert res.status_code == 404 # Usage as decorator @pook.on def test_request(): pook.mock('server.com/foo').reply(404) res = requests.get('server.com/foo') assert res.status_code == 404 """ return activate(fn) def disable(): """ Disables HTTP traffic interceptors. """ _engine.disable() def off(): """ Disables mock engine, HTTP traffic interceptors and flushed all the registered mocks. Internally, it calls ``pook.disable()`` and ``pook.off()``. """ disable() reset() def reset(): """ Resets current mock engine state, flushing all the registered mocks. This action will not disable the mock engine. """ _engine.reset() @contextmanager def use(network=False): """ Creates a new isolated mock engine to be used via context manager. Example:: with pook.use() as engine: pook.mock('server.com/foo').reply(404) res = requests.get('server.com/foo') assert res.status_code == 404 """ global _engine # Create temporal engine __engine = _engine activated = __engine.active if activated: __engine.disable() _engine = Engine(network=network) _engine.activate() # Yield enfine to be used by the context manager yield _engine # Restore engine state _engine.disable() if network: _engine.disable_network() # Restore previous engine _engine = __engine if activated: _engine.activate() @contextmanager def context(network=False): """ Create a new isolated mock engine to be used via context manager. Semantic alias to ``pook.context()``. Example:: with pook.use() as engine: pook.mock('server.com/foo').reply(404) res = requests.get('server.com/foo') assert res.status_code == 404 """ with use(network=network) as engine: yield engine @contextmanager def use_network(): """ Creates a new mock engine to be used as context manager Example:: with pook.use_network() as engine: pook.mock('server.com/foo').reply(404) res = requests.get('server.com/foo') assert res.status_code == 404 """ with use(network=True) as engine: yield engine def enable_network(*hostnames): """ Enables real networking mode for unmatched mocks in the current mock engine. """ _engine.enable_network(*hostnames) def disable_network(): """ Disables real traffic networking mode in the current mock engine. """ _engine.disable_network() def use_network_filter(*fn): """ Adds network filters to determine if certain outgoing unmatched HTTP traffic can stablish real network connections. Arguments: *fn (function): variadic function filter arguments to be used. """ _engine.use_network_filter(*fn) def flush_network_filters(): """ Flushes registered real networking filters in the current mock engine. """ _engine.flush_network_filters() def mock(url=None, **kw): """ Creates and register a new HTTP mock. Arguments: url (str): request URL to mock. activate (bool): force mock engine activation. Defaults to ``False``. **kw (mixed): variadic keyword arguments. Returns: pook.Mock: mock instance """ return _engine.mock(url, **kw) def get(url, **kw): """ Registers a new mock HTTP request with GET method. Arguments: url (str): request URL to mock. activate (bool): force mock engine activation. Defaults to ``False``. **kw (mixed): variadic arguments to ``pook.Mock`` constructor. Returns: pook.Mock: mock instance """ return mock(url, method='GET', **kw) def post(url, **kw): """ Registers a new mock HTTP request with POST method. Arguments: url (str): request URL to mock. activate (bool): force mock engine activation. Defaults to ``False``. **kw (mixed): variadic arguments to ``pook.Mock`` constructor. Returns: pook.Mock: mock instance """ return mock(url, method='POST', **kw) def put(url, **kw): """ Registers a new mock HTTP request with PUT method. Arguments: url (str): request URL to mock. activate (bool): force mock engine activation. Defaults to ``False``. **kw (mixed): variadic arguments to ``pook.Mock`` constructor. Returns: pook.Mock: mock instance """ return mock(url, method='PUT', **kw) def delete(url, **kw): """ Registers a new mock HTTP request with DELETE method. Arguments: url (str): request URL to mock. activate (bool): force mock engine activation. Defaults to ``False``. **kw (mixed): variadic arguments to ``pook.Mock`` constructor. Returns: pook.Mock: mock instance """ return mock(url, method='DELETE', **kw) def head(url, **kw): """ Registers a new mock HTTP request with HEAD method. Arguments: url (str): request URL to mock. activate (bool): force mock engine activation. Defaults to ``False``. **kw (mixed): variadic arguments to ``pook.Mock`` constructor. Returns: pook.Mock: mock instance """ return mock(url, method='HEAD', **kw) def patch(url=None, **kw): """ Registers a new mock HTTP request with PATCH method. Arguments: url (str): request URL to mock. activate (bool): force mock engine activation. Defaults to ``False``. **kw (mixed): variadic arguments to ``pook.Mock`` constructor. Returns: pook.Mock: new mock instance. """ return mock(url, method='PATCH', **kw) def options(url=None, **kw): """ Registers a new mock HTTP request with OPTIONS method. Arguments: url (str): request URL to mock. activate (bool): force mock engine activation. Defaults to ``False``. **kw (mixed): variadic arguments to ``pook.Mock`` constructor. Returns: pook.Mock: new mock instance. """ return mock(url, method='OPTIONS', **kw) def pending(): """ Returns the numbers of pending mocks to be matched. Returns: int: number of pending mocks to match. """ return _engine.pending() def ispending(): """ Returns the numbers of pending mocks to be matched. Returns: int: number of pending mocks to match. """ return _engine.ispending() def pending_mocks(): """ Returns pending mocks to be matched. Returns: list: pending mock instances. """ return _engine.pending_mocks() def unmatched_requests(): """ Returns a ``tuple`` of unmatched requests. Unmatched requests will be registered only if ``networking`` mode has been enabled. Returns: list: unmatched intercepted requests. """ return _engine.unmatched_requests() def unmatched(): """ Returns the total number of unmatched requests intercepted by pook. Unmatched requests will be registered only if ``networking`` mode has been enabled. Returns: int: total number of unmatched requests. """ return _engine.unmatched() def isunmatched(): """ Returns ``True`` if there are unmatched requests. Otherwise ``False``. Unmatched requests will be registered only if ``networking`` mode has been enabled. Returns: bool """ return _engine.isunmatched() def isactive(): """ Returns ``True`` if pook is active and intercepting traffic. Otherwise ``False``. Returns: bool: True is all the registered mocks are gone, otherwise False. """ return _engine.isactive() def isdone(): """ Returns True if all the registered mocks has been triggered. Returns: bool: True is all the registered mocks are gone, otherwise False. """ return _engine.isdone() def regex(expression, flags=re.IGNORECASE): """ Convenient shortcut to ``re.compile()`` for fast, easy to use regular expression compilation without an extra import statement. Arguments: expression (str): regular expression value. flags (int): optional regular expression flags. Defaults to ``re.IGNORECASE`` Returns: expression (str): string based regular expression. Raises: Exception: in case of regular expression compilation error Example:: (pook .get('api.com/foo') .header('Content-Type', pook.regex('[a-z]{1,4}'))) """ return re.compile(expression, flags=flags) pook-1.1.1/pook/assertion.py000066400000000000000000000034411435776471000160330ustar00rootroot00000000000000from unittest import TestCase from .regex import isregex, strip_regex, isregex_expr def test_case(): """ Creates a new ``unittest.TestCase`` instance. Returns: unittest.TestCase """ test = TestCase() test.maxDiff = None return test def equal(x, y): """ Shortcut function for ``unittest.TestCase.assertEqual()``. Arguments: x (mixed) y (mixed) Raises: AssertionError: in case of assertion error. Returns: bool """ return test_case().assertEqual(x, y) or True assert x == y def matches(x, y, regex_expr=False): """ Tries to match a regular expression value ``x`` against ``y``. Aliast``unittest.TestCase.assertEqual()`` Arguments: x (regex|str): regular expression to test. y (str): value to match. regex_expr (bool): enables regex string based expression matching. Raises: AssertionError: in case of mismatching. Returns: bool """ # Parse regex expression, if needed x = strip_regex(x) if regex_expr and isregex_expr(x) else x # Run regex assertion # Retrieve original regex pattern x = x.pattern if isregex(x) else x # Assert regular expression via unittest matchers return test_case().assertRegex(y, x) or True def test(x, y, regex_expr=False): """ Compares to values based on regular expression matching or strict equality comparison. Arguments: x (regex|str): string or regular expression to test. y (str): value to match. regex_expr (bool): enables regex string based expression matching. Raises: AssertionError: in case of matching error. Returns: bool """ return matches(x, y, regex_expr=regex_expr) if isregex(x) else equal(x, y) pook-1.1.1/pook/compare.py000066400000000000000000000025261435776471000154550ustar00rootroot00000000000000import re from .assertion import test # Negate is used a reserved token identifier to negate matching NEGATE = '!!' def compile(expr): try: return re.compile(expr, re.IGNORECASE) except Exception: pass def match(expr, value): regex = compile(expr) if not regex: return False return regex.match(value) is not None def strip_negate(value): return value[len(NEGATE):].lstrip() def compare(expr, value, regex_expr=False): """ Compares an string or regular expression againast a given value. Arguments: expr (str|regex): string or regular expression value to compare. value (str): value to compare against to. regex_expr (bool, optional): enables string based regex matching. Raises: AssertionError: in case of assertion error. Returns: bool """ # Strict equality comparison if expr == value: return True # Infer negate expression to match, if needed negate = False if isinstance(expr, str): negate = expr.startswith(NEGATE) expr = strip_negate(expr) if negate else expr try: # RegExp or strict equality comparison test(expr, value, regex_expr=regex_expr) except Exception as err: if negate: return True else: raise err return True pook-1.1.1/pook/constants.py000066400000000000000000000004711435776471000160400ustar00rootroot00000000000000# MIME type aliases for semantic convenience TYPES = { 'text': 'text/plain', 'html': 'text/html', 'json': 'application/json', 'xml': 'application/xml', 'urlencoded': 'application/x-www-form-urlencoded', 'form': 'application/x-www-form-urlencoded', 'form-data': 'application/x-www-form-urlencoded' } pook-1.1.1/pook/engine.py000066400000000000000000000332331435776471000152730ustar00rootroot00000000000000from functools import partial from inspect import isfunction from .mock import Mock from .regex import isregex from .mock_engine import MockEngine from .exceptions import PookNoMatches, PookExpiredMock class Engine(object): """ Engine represents the mock interceptor and matcher engine responsible of triggering interceptors and match outgoing HTTP traffic. Arguments: network (bool, optional): enables/disables real networking mode. Attributes: debug (bool): enables/disables debug mode. active (bool): stores the current engine activation status. networking (bool): stores the current engine networking mode status. mocks (list[pook.Mock]): stores engine mocks. filters (list[function]): stores engine-level mock filter functions. mappers (list[function]): stores engine-level mock mapper functions. interceptors (list[pook.BaseInterceptor]): stores engine-level HTTP traffic interceptors. unmatched_reqs (list[pook.Request]): stores engine-level unmatched outgoing HTTP requests. network_filters (list[function]): stores engine-level real networking mode filters. """ def __init__(self, network=False): # Enables/Disables debug mode. self.debug = True # Store the engine enable/disable status self.active = False # Enables/Disables real networking self.networking = network # Stores mocks self.mocks = [] # Store engine-level global filters self.filters = [] # Store engine-level global mappers self.mappers = [] # Store unmatched requests. self.unmatched_reqs = [] # Store network filters used to determine when a request # should be filtered or not. self.network_filters = [] # Built-in mock engine to be used self.mock_engine = MockEngine(self) def set_mock_engine(self, engine): """ Sets a custom mock engine, replacing the built-in one. This is particularly useful if you want to replace the built-in HTTP traffic mock interceptor engine with your custom one. For mock engine implementation details, see `pook.MockEngine`. Arguments: engine (pook.MockEngine): custom mock engine to use. """ if not engine: raise TypeError('engine must be a valid object') # Instantiate mock engine mock_engine = engine(self) # Validate minimum viable interface methods = ('activate', 'disable') if not all([hasattr(mock_engine, method) for method in methods]): raise NotImplementedError('engine must implementent the ' 'required methods') # Use the custom mock engine self.mock_engine = mock_engine # Enable mock engine, if needed if self.active: self.mock_engine.activate() def enable_network(self, *hostnames): """ Enables real networking mode, optionally passing one or multiple hostnames that would be used as filter. If at least one hostname matches with the outgoing traffic, the request will be executed via the real network. Arguments: *hostnames: optional list of host names to enable real network against them. hostname value can be a regular expression. """ def hostname_filter(hostname, req): if isregex(hostname): return hostname.match(req.url.hostname) return req.url.hostname == hostname for hostname in hostnames: self.use_network_filter(partial(hostname_filter, hostname)) self.networking = True def disable_network(self): """ Disables real networking mode. """ self.networking = False def use_network_filter(self, *fn): """ Adds network filters to determine if certain outgoing unmatched HTTP traffic can stablish real network connections. Arguments: *fn (function): variadic function filter arguments to be used. """ self.network_filters.extend(fn) def flush_network_filters(self): """ Flushes registered real networking filters in the current mock engine. """ self.network_filters = [] def mock(self, url=None, **kw): """ Creates and registers a new HTTP mock in the current engine. Arguments: url (str): request URL to mock. activate (bool): force mock engine activation. Defaults to ``False``. **kw (mixed): variadic keyword arguments for ``Mock`` constructor. Returns: pook.Mock: new mock instance. """ # Activate mock engine, if explicitly requested if kw.get('activate'): kw.pop('activate') self.activate() # Create the new HTTP mock expectation mock = Mock(url=url, **kw) # Expose current engine instance via mock mock._engine = self # Register the mock in the current engine self.add_mock(mock) # Return it for consumer satisfaction return mock def add_mock(self, mock): """ Adds a new mock instance to the current engine. Arguments: mock (pook.Mock): mock instance to add. """ self.mocks.append(mock) def remove_mock(self, mock): """ Removes a specific mock instance by object reference. Arguments: mock (pook.Mock): mock instance to remove. """ self.mocks = [m for m in self.mocks if m is not mock] def flush_mocks(self): """ Flushes the current mocks. """ self.mocks = [] def _engine_proxy(self, method, *args, **kw): engine_method = getattr(self.mock_engine, method, None) if not engine_method: raise NotImplementedError('current mock engine does not implements' ' required "{}" method'.format(method)) return engine_method(self.mock_engine, *args, **kw) def add_interceptor(self, *interceptors): """ Adds one or multiple HTTP traffic interceptors to the current mocking engine. Interceptors are typically HTTP client specific wrapper classes that implements the pook interceptor interface. Note: this method is may not be implemented if using a custom mock engine. Arguments: interceptors (pook.interceptors.BaseInterceptor) """ self._engine_proxy('add_interceptor', *interceptors) def flush_interceptors(self): """ Flushes registered interceptors in the current mocking engine. This method is low-level. Only call it if you know what you are doing. Note: this method is may not be implemented if using a custom mock engine. """ self._engine_proxy('flush_interceptors') def remove_interceptor(self, name): """ Removes a specific interceptor by name. Note: this method is may not be implemented if using a custom mock engine. Arguments: name (str): interceptor name to disable. Returns: bool: `True` if the interceptor was disabled, otherwise `False`. """ return self._engine_proxy('remove_interceptor', name) def activate(self): """ Activates the registered interceptors in the mocking engine. This means any HTTP traffic captures by those interceptors will trigger the HTTP mock matching engine in order to determine if a given HTTP transaction should be mocked out or not. """ if self.active: return None # Activate mock engine self.mock_engine.activate() # Enable engine state self.active = True def disable(self): """ Disables interceptors and stops intercepting any outgoing HTTP traffic. """ if not self.active: return None # Disable current mock engine self.mock_engine.disable() # Disable engine state self.active = False def reset(self): """ Resets and flushes engine state and mocks to defaults. """ # Reset engine Engine.__init__(self, network=self.networking) def unmatched_requests(self): """ Returns a ``tuple`` of unmatched requests. Unmatched requests will be registered only if ``networking`` mode has been enabled. Returns: list: unmatched intercepted requests. """ return [mock for mock in self.unmatched_reqs] def unmatched(self): """ Returns the total number of unmatched requests intercepted by pook. Unmatched requests will be registered only if ``networking`` mode has been enabled. Returns: int: total number of unmatched requests. """ return len(self.unmatched_requests()) def isunmatched(self): """ Returns ``True`` if there are unmatched requests. Otherwise ``False``. Unmatched requests will be registered only if ``networking`` mode has been enabled. Returns: bool """ return len(self.unmatched()) > 0 def pending(self): """ Returns the number of pending mocks to be matched. Returns: int: number of pending mocks. """ return len(self.pending_mocks()) def pending_mocks(self): """ Returns a ``tuple`` of pending mocks to be matched. Returns: tuple: pending mock instances. """ return [mock for mock in self.mocks if not mock.isdone()] def ispending(self): """ Returns the ``True`` if the engine has pending mocks to be matched. Otherwise ``False``. Returns: bool """ return len(self.pending_mocks()) def isactive(self): """ Returns the current engine enabled/disabled status. Returns: bool: ``True`` if the engine is active. Otherwise ``False``. """ return self.active def isdone(self): """ Returns True if all the registered mocks has been triggered. Returns: bool: True is all the registered mocks are gone, otherwise False. """ return all(mock.isdone() for mock in self.mocks) def _append(self, target, *fns): (target.append(fn) for fn in fns if isfunction(fn)) def filter(self, *filters): """ Append engine-level HTTP request filter functions. Arguments: filters*: variadic filter functions to be added. """ self._append(self.filters, *filters) def map(self, *mappers): """ Append engine-level HTTP request mapper functions. Arguments: filters*: variadic mapper functions to be added. """ self._append(self.mappers, *mappers) def should_use_network(self, request): """ Verifies if real networking mode should be used for the given request, passing it to the registered network filters. Arguments: request (pook.Request): outgoing HTTP request to test. Returns: bool """ return (self.networking and all((fn(request) for fn in self.network_filters))) def match(self, request): """ Matches a given Request instance contract against the registered mocks. If a mock passes all the matchers, its response will be returned. Arguments: request (pook.Request): Request contract to match. Raises: pook.PookNoMatches: if networking is disabled and no mock matches with the given request contract. Returns: pook.Response: the mock response to be used by the interceptor. """ # Trigger engine-level request filters for test in self.filters: if not test(request, self): return False # Trigger engine-level request mappers for mapper in self.mappers: request = mapper(request, self) if not request: raise ValueError('map function must return a request object') # Store list of mock matching errors for further debugging match_errors = [] # Try to match the request against registered mock definitions for mock in self.mocks[:]: try: # Return the first matched HTTP request mock matches, errors = mock.match(request.copy()) if len(errors): match_errors += errors if matches: return mock except PookExpiredMock: # Remove the mock if already expired self.mocks.remove(mock) # Validate that we have a mock if not self.should_use_network(request): msg = 'pook error!\n\n' msg += ( '=> Cannot match any mock for the ' 'following request:\n{}'.format(request) ) # Compose unmatch error details, if debug mode is enabled if self.debug: err = '\n\n'.join([str(err) for err in match_errors]) if err: msg += '\n\n=> Detailed matching errors:\n{}\n'.format(err) # Raise no matches exception raise PookNoMatches(msg) # Register unmatched request self.unmatched_reqs.append(request) pook-1.1.1/pook/exceptions.py000066400000000000000000000003501435776471000162010ustar00rootroot00000000000000class PookInvalidBody(Exception): pass class PookNoMatches(Exception): pass class PookNetworkFilterError(Exception): pass class PookExpiredMock(Exception): pass class PookInvalidArgument(Exception): pass pook-1.1.1/pook/headers.py000066400000000000000000000170471435776471000154460ustar00rootroot00000000000000try: from collections.abc import Mapping, MutableMapping except ImportError: from collections import Mapping, MutableMapping class HTTPHeaderDict(MutableMapping): """ :param headers: An iterable of field-value pairs. Must not contain multiple field names when compared case-insensitively. :param kwargs: Additional field-value pairs to pass in to ``dict.update``. A ``dict`` like container for storing HTTP Headers. Field names are stored and compared case-insensitively in compliance with RFC 7230. Iteration provides the first case-sensitive key seen for each case-insensitive pair. Using ``__setitem__`` syntax overwrites fields that compare equal case-insensitively in order to maintain ``dict``'s api. For fields that compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add`` in a loop. If multiple fields that are equal case-insensitively are passed to the constructor or ``.update``, the behavior is undefined and some will be lost. Usage:: headers = HTTPHeaderDict() headers.add('Set-Cookie', 'foo=bar') headers.add('set-cookie', 'baz=quxx') headers['content-length'] = '7' headers['SET-cookie'] > 'foo=bar, baz=quxx' headers['Content-Length'] > '7' """ def __init__(self, headers=None, **kwargs): super(HTTPHeaderDict, self).__init__() self._container = {} if headers is not None: if isinstance(headers, HTTPHeaderDict): self._copy_from(headers) else: self.extend(headers) if kwargs: self.extend(kwargs) def __setitem__(self, key, val): self._container[key.lower()] = (key, val) return self._container[key.lower()] def __getitem__(self, key): val = self._container[key.lower()] return ', '.join(val[1:]) def __delitem__(self, key): del self._container[key.lower()] def __contains__(self, key): return key.lower() in self._container def __eq__(self, other): if not isinstance(other, Mapping) and not hasattr(other, 'keys'): return False if not isinstance(other, type(self)): other = type(self)(other) return (dict((k.lower(), v) for k, v in self.itermerged()) == dict((k.lower(), v) for k, v in other.itermerged())) def __ne__(self, other): return not self.__eq__(other) __marker = object() def __len__(self): return len(self._container) def __iter__(self): # Only provide the originally cased names for vals in self._container.values(): yield vals[0] def pop(self, key, default=__marker): """ D.pop(k[,d]) -> v, remove specified key and return the corresponding value. If key is not found, d is returned if given, otherwise KeyError is raised. """ # Using the MutableMapping function directly fails due to the # private marker. Using ordinary dict.pop would expose the # internal structures. So let's reinvent the wheel. try: value = self[key] except KeyError: if default is self.__marker: raise return default else: del self[key] return value def discard(self, key): try: del self[key] except KeyError: pass def add(self, key, val): """ Adds a (name, value) pair, doesn't overwrite the value if it already exists. Usage:: headers = HTTPHeaderDict(foo='bar') headers.add('Foo', 'baz') headers['foo'] > 'bar, baz' """ key_lower = key.lower() new_vals = key, val # Keep the common case aka no item present as fast as possible vals = self._container.setdefault(key_lower, new_vals) if new_vals is not vals: # new_vals was not inserted, as there was a previous one if isinstance(vals, list): # If already several items got inserted, we have a list vals.append(val) else: # vals should be a tuple then, i.e. only one item so far # Need to convert the tuple to list for further extension self._container[key_lower] = [vals[0], vals[1], val] def set(self, key, val): """ Sets a header field with the given value, removing previous values. Usage:: headers = HTTPHeaderDict(foo='bar') headers.set('Foo', 'baz') headers['foo'] > 'baz' """ key_lower = key.lower() new_vals = key, val # Keep the common case aka no item present as fast as possible vals = self._container.setdefault(key_lower, new_vals) if new_vals is not vals: self._container[key_lower] = [vals[0], vals[1], val] def extend(self, *args, **kwargs): """ Generic import function for any type of header-like object. Adapted version of MutableMapping.update in order to insert items with self.add instead of self.__setitem__ """ if len(args) > 1: raise TypeError("extend() takes at most 1 positional " "arguments ({0} given)".format(len(args))) other = args[0] if len(args) >= 1 else () if isinstance(other, HTTPHeaderDict): for key, val in other.iteritems(): self.add(key, val) elif isinstance(other, Mapping): for key in other: self.add(key, other[key]) elif hasattr(other, "keys"): for key in other.keys(): self.add(key, other[key]) else: for key, value in other: self.add(key, value) for key, value in kwargs.items(): self.add(key, value) def getlist(self, key): """ Returns a list of all the values for the named field. Returns an empty list if the key doesn't exist. """ try: vals = self._container[key.lower()] except KeyError: return [] else: if isinstance(vals, tuple): return [vals[1]] else: return vals[1:] # Backwards compatibility for httplib getheaders = getlist getallmatchingheaders = getlist iget = getlist def __repr__(self): return "%s(%s)" % (type(self).__name__, dict(self.itermerged())) def _copy_from(self, other): for key in other: val = other.getlist(key) if isinstance(val, list): # Don't need to convert tuples val = list(val) self._container[key.lower()] = [key] + val def copy(self): clone = type(self)() clone._copy_from(self) return clone def iteritems(self): """ Iterate over all header lines, including duplicate ones. """ for key in self: vals = self._container[key.lower()] for val in vals[1:]: yield vals[0], val def itermerged(self): """ Iterate over all headers, merging duplicate ones together. """ for key in self: val = self._container[key.lower()] yield val[0], ', '.join(val[1:]) def items(self): return list(self.iteritems()) def to_dict(self): return {key: values for key, values in self.items()} pook-1.1.1/pook/helpers.py000066400000000000000000000025341435776471000154700ustar00rootroot00000000000000from inspect import ismethod, isfunction from .exceptions import PookInvalidArgument def trigger_methods(instance, args): """" Triggers specific class methods using a simple reflection mechanism based on the given input dictionary params. Arguments: instance (object): target instance to dynamically trigger methods. args (iterable): input arguments to trigger objects to Returns: None """ # Start the magic for name in sorted(args): value = args[name] target = instance # If response attibutes if name.startswith('response_') or name.startswith('reply_'): name = name.replace('response_', '').replace('reply_', '') # If instance has response attribute, use it if hasattr(instance, '_response'): target = instance._response # Retrieve class member for inspection and future use member = getattr(target, name, None) # Is attribute isattr = name in dir(target) iscallable = ismethod(member) and not isfunction(member) if not iscallable and not isattr: raise PookInvalidArgument('Unsupported argument: {}'.format(name)) # Set attribute or trigger method if iscallable: member(value) else: setattr(target, name, value) pook-1.1.1/pook/interceptors/000077500000000000000000000000001435776471000161715ustar00rootroot00000000000000pook-1.1.1/pook/interceptors/__init__.py000066400000000000000000000021361435776471000203040ustar00rootroot00000000000000from .urllib3 import Urllib3Interceptor from .http import HTTPClientInterceptor from .base import BaseInterceptor # Explicit symbols to export __all__ = ( 'interceptors', 'add', 'get', 'BaseInterceptor', 'Urllib3Interceptor', 'HTTPClientInterceptor', 'AIOHTTPInterceptor', ) # Store built-in interceptors in pook. interceptors = [ Urllib3Interceptor, HTTPClientInterceptor ] try: import aiohttp # noqa from .aiohttp import AIOHTTPInterceptor interceptors.append(AIOHTTPInterceptor) except ImportError: pass def add(*custom_interceptors): """ Registers a new HTTP client interceptor. Arguments: *custom_interceptors (interceptor): interceptor(s) to be added. """ interceptors.append(*custom_interceptors) def get(name): """ Returns an interceptor by class name. Arguments: name (str): interceptor class name or alias. Returns: interceptor: found interceptor instance, otherwise ``None``. """ for interceptor in interceptors: if interceptor.__name__ == name: return interceptor pook-1.1.1/pook/interceptors/aiohttp.py000066400000000000000000000117471435776471000202250ustar00rootroot00000000000000from ..request import Request from .base import BaseInterceptor from unittest import mock from urllib.parse import urlunparse, urlencode from http.client import responses as http_reasons import asyncio from aiohttp.helpers import TimerNoop from aiohttp.streams import EmptyStreamReader # Try to load yarl URL parser package used by aiohttp try: import yarl import multidict except Exception: yarl, multidict = None, None PATCHES = ( 'aiohttp.client.ClientSession._request', ) RESPONSE_CLASS = 'ClientResponse' RESPONSE_PATH = 'aiohttp.client_reqrep' class SimpleContent(EmptyStreamReader): def __init__(self, content, *args, **kwargs): super().__init__(*args, **kwargs) self.content = content async def read(self, n=-1): return self.content def HTTPResponse(*args, **kw): # Dynamically load package module = __import__(RESPONSE_PATH, fromlist=(RESPONSE_CLASS,)) ClientResponse = getattr(module, RESPONSE_CLASS) # Return response instance return ClientResponse( *args, request_info=mock.Mock(), writer=mock.Mock(), continue100=None, timer=TimerNoop(), traces=[], loop=mock.Mock(), session=mock.Mock(), **kw ) class AIOHTTPInterceptor(BaseInterceptor): """ aiohttp HTTP client traffic interceptor. """ def _url(self, url): return yarl.URL(url) if yarl else None async def _on_request(self, _request, session, method, url, data=None, headers=None, **kw): # Create request contract based on incoming params req = Request(method) req.headers = headers or {} req.body = data # Expose extra variadic arguments req.extra = kw # Compose URL if not kw.get('params'): req.url = str(url) else: req.url = str(url) + '?' + urlencode( [(x, y) for x, y in kw['params'].items()] ) # Match the request against the registered mocks in pook mock = self.engine.match(req) # If cannot match any mock, run real HTTP request if networking # or silent model are enabled, otherwise this statement won't # be reached (an exception will be raised before). if not mock: return await _request(session, method, url, data=data, headers=headers, **kw) # Simulate network delay if mock._delay: await asyncio.sleep(mock._delay / 1000) # noqa # Shortcut to mock response res = mock._response # Aggregate headers as list of tuples for interface compatibility headers = [] for key in res._headers: headers.append((key, res._headers[key])) # Create mock equivalent HTTP response _res = HTTPResponse(req.method, self._url(urlunparse(req.url))) # response status _res.version = (1, 1) _res.status = res._status _res.reason = http_reasons.get(res._status) _res._should_close = False # Add response headers _res._raw_headers = tuple(headers) _res._headers = multidict.CIMultiDictProxy( multidict.CIMultiDict(headers) ) if res._body: _res.content = SimpleContent( res._body.encode('utf-8', errors='replace'), ) else: # Define `_content` attribute with an empty string to # force do not read from stream (which won't exists) _res.content = EmptyStreamReader() # Return response based on mock definition return _res def _patch(self, path): # If not able to import aiohttp dependencies, skip if not yarl or not multidict: return None async def handler(session, method, url, data=None, headers=None, **kw): return await self._on_request( _request, session, method, url, data=data, headers=headers, **kw) try: # Create a new patcher for Urllib3 urlopen function # used as entry point for all the HTTP communications patcher = mock.patch(path, handler) # Retrieve original patched function that we might need for real # networking _request = patcher.get_original()[0] # Start patching function calls patcher.start() except Exception: # Exceptions may accur due to missing package # Ignore all the exceptions for now pass else: self.patchers.append(patcher) def activate(self): """ Activates the traffic interceptor. This method must be implemented by any interceptor. """ [self._patch(path) for path in PATCHES] def disable(self): """ Disables the traffic interceptor. This method must be implemented by any interceptor. """ [patch.stop() for patch in self.patchers] pook-1.1.1/pook/interceptors/base.py000066400000000000000000000014461435776471000174620ustar00rootroot00000000000000from abc import abstractmethod, ABCMeta class BaseInterceptor(object): """ BaseInterceptor provides a base class for HTTP traffic interceptors implementations. """ __metaclass__ = ABCMeta def __init__(self, engine): self.patchers = [] self.engine = engine @property def name(self): """ Exposes the interceptor class name. """ return type(self).__name__ @abstractmethod def activate(self): """ Activates the traffic interceptor. This method must be implemented by any interceptor. """ pass @abstractmethod def disable(self): """ Disables the traffic interceptor. This method must be implemented by any interceptor. """ pass pook-1.1.1/pook/interceptors/http.py000066400000000000000000000101011435776471000175130ustar00rootroot00000000000000import socket from ..request import Request from .base import BaseInterceptor from unittest import mock from http.client import responses as http_reasons, _CS_REQ_SENT PATCHES = ( 'http.client.HTTPConnection.request', ) RESPONSE_CLASS = 'HTTPResponse' RESPONSE_PATH = 'http.client' URLLIB3_BYPASS = '__urllib3_bypass__' def HTTPResponse(*args, **kw): # Dynamically load package module = __import__(RESPONSE_PATH, fromlist=(RESPONSE_CLASS,)) HTTPResponse = getattr(module, RESPONSE_CLASS) # Return response instance return HTTPResponse(*args, **kw) class SocketMock(socket.socket): def __init__(self): pass def makefile(self, *args, **kw): pass def close(self, *args, **kw): pass class HTTPClientInterceptor(BaseInterceptor): """ urllib / http.client HTTP traffic interceptor. """ def _on_request(self, _request, conn, method, url, body=None, headers=None, **kw): # Create request contract based on incoming params req = Request(method) req.headers = headers or {} req.body = body # Compose URL req.url = 'http://{}:{}{}'.format(conn.host, conn.port, url) # Match the request against the registered mocks in pook mock = self.engine.match(req) # If cannot match any mock, run real HTTP request since networking, # otherwise this statement won't be reached # (an exception will be raised before). if not mock: return _request(conn, method, url, body=body, headers=headers, **kw) # Shortcut to mock response res = mock._response # Aggregate headers as list of tuples for interface compatibility headers = [] for key in res._headers: headers.append((key, res._headers[key])) mockres = HTTPResponse(SocketMock(), method=method, url=url) mockres.version = (1, 1) mockres.status = res._status mockres.reason = http_reasons.get(res._status) mockres.headers = res._headers.to_dict() def getresponse(): return mockres conn.getresponse = getresponse conn.__response = mockres conn.__state = _CS_REQ_SENT # Path reader def read(): return res._body or '' mockres.read = read return mockres def _patch(self, path): def handler(conn, method, url, body=None, headers=None, **kw): # Detect if httplib was called by urllib3 interceptor # This is a bit ugly, I know. Ideas are welcome! if headers and URLLIB3_BYPASS in headers: # Remove bypass header used as flag headers.pop(URLLIB3_BYPASS) # Call original patched function return request(conn, method, url, body=body, headers=headers, **kw) # Otherwise call the request interceptor return self._on_request(request, conn, method, url, body=body, headers=headers, **kw) try: # Create a new patcher for Urllib3 urlopen function # used as entry point for all the HTTP communications patcher = mock.patch(path, handler) # Retrieve original patched function that we might need for real # networking request = patcher.get_original()[0] # Start patching function calls patcher.start() except Exception: # Exceptions may accur due to missing package # Ignore all the exceptions for now pass else: self.patchers.append(patcher) def activate(self): """ Activates the traffic interceptor. This method must be implemented by any interceptor. """ [self._patch(path) for path in PATCHES] def disable(self): """ Disables the traffic interceptor. This method must be implemented by any interceptor. """ [patch.stop() for patch in self.patchers] pook-1.1.1/pook/interceptors/pycurl.py000066400000000000000000000002231435776471000200560ustar00rootroot00000000000000from .base import BaseInterceptor class PyCURLInterceptor(BaseInterceptor): """ PyCURL HTTP client traffic interceptor. """ pass pook-1.1.1/pook/interceptors/urllib3.py000066400000000000000000000141371435776471000201250ustar00rootroot00000000000000import io from ..request import Request from .base import BaseInterceptor from .http import URLLIB3_BYPASS from unittest import mock from http.client import ( responses as http_reasons, HTTPResponse as ClientHTTPResponse, ) PATCHES = ( 'requests.packages.urllib3.connectionpool.HTTPConnectionPool.urlopen', 'urllib3.connectionpool.HTTPConnectionPool.urlopen' ) RESPONSE_CLASS = 'HTTPResponse' RESPONSE_PATH = { 'requests': 'requests.packages.urllib3.response', 'urllib3': 'urllib3.response' } def HTTPResponse(path, *args, **kw): # Infer package package = path.split('.').pop(0) # Get import path import_path = RESPONSE_PATH.get(package) # Dynamically load package module = __import__(import_path, fromlist=(RESPONSE_CLASS,)) HTTPResponse = getattr(module, RESPONSE_CLASS) # Return response instance return HTTPResponse(*args, **kw) def body_io(string, encoding='utf-8'): if hasattr(string, 'encode'): string = string.encode(encoding) return io.BytesIO(string) def is_chunked_response(headers): tencoding = dict(headers).get("Transfer-Encoding", "").lower() return "chunked" in tencoding.split(",") class MockSock(object): @classmethod def makefile(cls, *args, **kwargs): return class FakeHeaders(list): def get_all(self, key, default=None): key = key.lower() return [v for (k, v) in self if k.lower() == key] getheaders = get_all class FakeResponse(object): def __init__(self, method, headers): self._method = method # name expected by urllib3 self.msg = FakeHeaders(headers) self.closed = False def close(self): self.closed = True def isclosed(self): return self.closed class FakeChunkedResponseBody(object): def __init__(self, chunks): # append a terminating chunk chunks.append(b'') self.position = 0 self.stream = b''.join([self._encode(c) for c in chunks]) self.closed = False def _encode(self, chunk): length = '%X\r\n' % len(chunk) return length.encode() + chunk + b'\r\n' def read_chunk(self, amt=-1, whole=False): if whole or amt == -1: end_idx = self.stream.index(b'\r\n', self.position) + 2 else: end_idx = self.position + amt chunk = self.stream[self.position:end_idx] self.position = end_idx return chunk def readline(self): return self.read_chunk(whole=True) def read(self, amt=-1): return self.read_chunk(amt) def flush(self): pass def close(self): self.closed = True class Urllib3Interceptor(BaseInterceptor): """ Urllib3 HTTP traffic interceptor. """ def _on_request(self, urlopen, path, pool, method, url, body=None, headers=None, **kw): # Remove bypass headers real_headers = dict(headers or {}) real_headers.pop(URLLIB3_BYPASS) # Create request contract based on incoming params req = Request(method) req.headers = real_headers req.body = body # Compose URL req.url = '{}://{}:{:d}{}'.format( pool.scheme, pool.host, pool.port or 80, url ) # Match the request against the registered mocks in pook mock = self.engine.match(req) # If cannot match any mock, run real HTTP request since networking # or silent model will be enabled, otherwise this statement won't # be reached (an exception will be raised before). if not mock: return urlopen(pool, method, url, body=body, headers=headers, **kw) # Shortcut to mock response and response body res = mock._response body = res._body # Aggregate headers as list of tuples for interface compatibility headers = [] for key in res._headers: headers.append((key, res._headers[key])) if is_chunked_response(headers): body_chunks = body if isinstance(body, list) else [body] body_chunks = [chunk.encode() for chunk in body_chunks] body = ClientHTTPResponse(MockSock) body.fp = FakeChunkedResponseBody(body_chunks) else: # Assume that the body is a bytes-like object body = body_io(body) # Return mocked HTTP response return HTTPResponse( path, body=body, status=res._status, headers=headers, preload_content=False, reason=http_reasons.get(res._status), original_response=FakeResponse(method, headers), ) def _patch(self, path): def handler(conn, method, url, body=None, headers=None, **kw): # Flag that the current request as urllib3 intercepted headers = headers or {} headers[URLLIB3_BYPASS] = True # Call request interceptor return self._on_request(urlopen, path, conn, method, url, body=body, headers=headers, **kw) try: # Create a new patcher for Urllib3 urlopen function # used as entry point for all the HTTP communications patcher = mock.patch(path, handler) # Retrieve original patched function that we might need for real # networking urlopen = patcher.get_original()[0] # Start patching function calls patcher.start() except Exception: # Exceptions may accur due to missing package # Ignore all the exceptions for now pass else: self.patchers.append(patcher) def activate(self): """ Activates the traffic interceptor. This method must be implemented by any interceptor. """ [self._patch(path) for path in PATCHES] def disable(self): """ Disables the traffic interceptor. This method must be implemented by any interceptor. """ patchers_reversed = self.patchers[::-1] [patch.stop() for patch in patchers_reversed] pook-1.1.1/pook/matcher.py000066400000000000000000000031641435776471000154510ustar00rootroot00000000000000class MatcherEngine(list): """ HTTP request matcher engine used by `pook.Mock` to test if an intercepted outgoing HTTP request must be mocked out or not. """ def add(self, matcher): """ Adds a new matcher function to the current engine. Arguments: matcher (function): matcher function to be added. """ self.append(matcher) def flush(self): """ Flushes the current matcher engine, removing all the registered matcher functions. """ self.clear() def match(self, request): """ Match the given HTTP request instance against the registered matcher functions in the current engine. Arguments: request (pook.Request): outgoing request to match. Returns: tuple(bool, list[Exception]): ``True`` if all matcher tests passes, otherwise ``False``. Also returns an optional list of error exceptions. """ errors = [] def match(matcher): try: return matcher.match(request) except Exception as err: err = '{}: {}'.format(type(matcher).__name__, err) errors.append(err) return False return all([match(matcher) for matcher in self]), errors def __repr__(self): """ Returns an human friendly readable instance data representation. Returns: str """ matchers = [repr(matcher) for matcher in self] return 'MatcherEngine([\n {}\n])'.format(',\n '.join(matchers)) pook-1.1.1/pook/matchers/000077500000000000000000000000001435776471000152565ustar00rootroot00000000000000pook-1.1.1/pook/matchers/__init__.py000066400000000000000000000000331435776471000173630ustar00rootroot00000000000000from .api import * # noqa pook-1.1.1/pook/matchers/api.py000066400000000000000000000037451435776471000164120ustar00rootroot00000000000000from .base import BaseMatcher from .url import URLMatcher from .body import BodyMatcher from .query import QueryMatcher from .method import MethodMatcher from .headers import HeadersMatcher from .path import PathMatcher from .xml import XMLMatcher from .json import JSONMatcher from .json_schema import JSONSchemaMatcher # Explicit symbols to export __all__ = ( 'init', 'add', 'get', 'matchers', 'BaseMatcher', 'MethodMatcher', 'URLMatcher', 'HeadersMatcher', 'QueryMatcher', 'PathMatcher', 'BodyMatcher', 'XMLMatcher', 'JSONMatcher', 'JSONSchemaMatcher', 'QueryMatcher', ) # List of built-in matchers # This is intended to be mutable. matchers = [ MethodMatcher, URLMatcher, HeadersMatcher, QueryMatcher, PathMatcher, BodyMatcher, XMLMatcher, JSONMatcher, JSONSchemaMatcher, QueryMatcher, ] def add(*matcher): """ Registers one or multiple matchers to be used by default from mocking engine. Arguments: *matcher (list[pook.BaseMatcher]): variadic matchers to add. """ [matchers.append(m) for m in matcher] def get(name): """ Returns a matcher instance by class or alias name. Arguments: name (str): matcher class name or alias. Returns: matcher: found matcher instance, otherwise ``None``. """ for matcher in matchers: if matcher.__name__ == name or getattr(matcher, 'name', None) == name: return matcher def init(name, *args): """ Initializes a matcher instance passing variadic arguments to its constructor. Acts as a delegator proxy. Arguments: name (str): matcher class name or alias to execute. *args (mixed): variadic argument Returns: matcher: matcher instance. Raises: ValueError: if matcher was not found. """ matcher = get(name) if not matcher: raise ValueError('Cannot find matcher: {}'.format(name)) return matcher(*args) pook-1.1.1/pook/matchers/base.py000066400000000000000000000040311435776471000165400ustar00rootroot00000000000000import functools from copy import deepcopy from abc import abstractmethod, ABCMeta from ..compare import compare class BaseMatcher(object): """ BaseMatcher implements the basic HTTP request matching interface. """ __metaclass__ = ABCMeta # Negate matching if necessary negate = False def __init__(self, expectation, negate=False): if not expectation: raise ValueError('expectation argument cannot be empty') self.negate = negate self._expectation = expectation @property def name(self): return type(self).__name__ @property def expectation(self): return self._expectation @expectation.setter def expectation(self, value): self._expectation = value @abstractmethod def match(self, request): """ Match performs the value matching. This is an abstract method that must be implemented by child classes. Arguments: request (pook.Request): request object to match. """ pass def compare(self, value, expectation, regex_expr=False): """ Compares two values with regular expression matching support. Arguments: value (mixed): value to compare. expectation (mixed): value to match. regex_expr (bool, optional): enables string based regex matching. Returns: bool """ return compare(value, expectation, regex_expr=regex_expr) def to_dict(self): """ Returns the current matcher representation as dictionary. Returns: dict """ return {self.name: deepcopy(self.expectation)} def __repr__(self): return '{}({})'.format(self.name, self.expectation) def __str__(self): return self.expectation @staticmethod def matcher(fn): @functools.wraps(fn) def wrapper(self, *args): result = fn(self, *args) return not result if self.negate else result return wrapper pook-1.1.1/pook/matchers/body.py000066400000000000000000000007261435776471000165720ustar00rootroot00000000000000from .base import BaseMatcher class BodyMatcher(BaseMatcher): """ BodyMatchers matches the request body via strict value comparison or regular expression based matching. """ @BaseMatcher.matcher def match(self, req): expectation = self.expectation # Decode bytes input if isinstance(expectation, bytes): expectation = expectation.decode('utf-8') return self.compare(self.expectation, req.body) pook-1.1.1/pook/matchers/headers.py000066400000000000000000000012771435776471000172520ustar00rootroot00000000000000from .base import BaseMatcher class HeadersMatcher(BaseMatcher): """ Headers HTTP request matcher. """ def __init__(self, headers): if not isinstance(headers, dict): raise TypeError('headers must be a dictionary') BaseMatcher.__init__(self, headers) @BaseMatcher.matcher def match(self, req): for key in self.expectation: # Retrieve value to match value = self.expectation[key] # Retrieve header value by key header = req.headers.get(key) # Compare header value if not self.compare(value, header, regex_expr=True): return False return True pook-1.1.1/pook/matchers/json.py000066400000000000000000000013611435776471000166020ustar00rootroot00000000000000import json from .base import BaseMatcher class JSONMatcher(BaseMatcher): """ JSONMatcher implements a JSON body matcher supporting strict structure and regular expression based comparisons. """ def __init__(self, data): BaseMatcher.__init__(self, data) if isinstance(data, str): self.expectation = json.loads(data) @BaseMatcher.matcher def match(self, req): body = req.body if isinstance(body, str): try: body = json.loads(body) except Exception: return False x = json.dumps(self.expectation, sort_keys=True, indent=4) y = json.dumps(body, sort_keys=True, indent=4) return self.compare(x, y) pook-1.1.1/pook/matchers/json_schema.py000066400000000000000000000014341435776471000201230ustar00rootroot00000000000000import json from jsonschema import validate from .base import BaseMatcher class JSONSchemaMatcher(BaseMatcher): """ JSONSchema matcher validates a request body against a given JSONSchema definition schema. """ def __init__(self, schema): BaseMatcher.__init__(self, schema) if isinstance(schema, str): self.expectation = json.loads(schema) @BaseMatcher.matcher def match(self, req): body = req.body if isinstance(body, str): try: body = json.loads(body) except Exception: return False if not body: return False try: validate(body, self.expectation) except Exception: return False return True pook-1.1.1/pook/matchers/method.py000066400000000000000000000004331435776471000171100ustar00rootroot00000000000000from .base import BaseMatcher class MethodMatcher(BaseMatcher): """ MethodMatcher implements. """ @BaseMatcher.matcher def match(self, req): return (self.expectation == '*' or self.compare(req.method.lower(), self.expectation.lower())) pook-1.1.1/pook/matchers/path.py000066400000000000000000000003601435776471000165630ustar00rootroot00000000000000from .base import BaseMatcher class PathMatcher(BaseMatcher): """ PathMatcher implements an URL path matcher. """ @BaseMatcher.matcher def match(self, req): return self.compare(self.expectation, req.url.path) pook-1.1.1/pook/matchers/query.py000066400000000000000000000022251435776471000167760ustar00rootroot00000000000000from .base import BaseMatcher from urllib.parse import parse_qs class QueryMatcher(BaseMatcher): """ QueryMatcher implements an URL query params matcher. """ def match_query(self, query, req_query): def test(key, param): match = req_query.get(key) if match is None: return False # Normalize param value param = [param] if not isinstance(param, list) else param # Compare query params [[self.compare(value, expect) for expect in match] for value in param] return True return all([test(key, query[key]) for key in query]) @BaseMatcher.matcher def match(self, req): query = self.expectation # Parse and assert type if isinstance(query, str): query = parse_qs(self.expectation) # Validate query params if not isinstance(query, dict): raise ValueError('query params must be a str or dict') # Parse request URL query req_query = parse_qs(req.url.query) # Match query params return self.match_query(query, req_query) pook-1.1.1/pook/matchers/url.py000066400000000000000000000035071435776471000164370ustar00rootroot00000000000000import re from .base import BaseMatcher from .path import PathMatcher from .query import QueryMatcher from ..regex import isregex from urllib.parse import urlparse # URI protocol test regular expression protoregex = re.compile('^http[s]?://', re.IGNORECASE) class URLMatcher(BaseMatcher): """ URLMatcher implements an URL schema matcher. """ # Matches URL as regular expression regex = False def __init__(self, url): if not url: raise ValueError('url argument cannot be empty') # Store original URL value self.url = url # Process as regex value if isregex(url): self.regex = True self.expectation = url else: # Add protocol prefix in the URL if not protoregex.match(url): self.url = 'http://{}'.format(url) self.expectation = urlparse(self.url) def match_path(self, req): path = self.expectation.path if not path: return True return PathMatcher(path).match(req) def match_query(self, req): query = self.expectation.query if not query: return True return QueryMatcher(query).match(req) @BaseMatcher.matcher def match(self, req): url = self.expectation # Match as regex if self.regex: return self.compare(url, req.url.geturl(), regex_expr=True) # Match URL return all([ self.compare(url.scheme, req.url.scheme), self.compare(url.hostname, req.url.hostname), self.compare(url.port or req.url.port, req.url.port), self.match_path(req), self.match_query(req) ]) def __str__(self): return self.url def __repr__(self): return '{}({})'.format(self.name, self.url) pook-1.1.1/pook/matchers/xml.py000066400000000000000000000013341435776471000164310ustar00rootroot00000000000000import json import xmltodict from .base import BaseMatcher class XMLMatcher(BaseMatcher): """ XMLMatcher implements a XML body matcher supporting both strict structure comparison and regular expression. """ def __init__(self, data): BaseMatcher.__init__(self, data) if isinstance(data, str): self.expectation = xmltodict.parse(data) def compare(self, data): x = json.dumps(xmltodict.parse(data), sort_keys=True) y = json.dumps(self.expectation, sort_keys=True) return x == y @BaseMatcher.matcher def match(self, req): data = req.body if not isinstance(data, str): return False return self.compare(data) pook-1.1.1/pook/mock.py000066400000000000000000000622661435776471000147670ustar00rootroot00000000000000import re import functools from furl import furl from inspect import isfunction, ismethod from .response import Response from .constants import TYPES from .request import Request from .matcher import MatcherEngine from .helpers import trigger_methods from .exceptions import PookExpiredMock from .matchers import init as matcher def _append_funcs(target, items): """ Helper function to append functions into a given list. Arguments: target (list): receptor list to append functions. items (iterable): iterable that yields elements to append. """ [target.append(item) for item in items if isfunction(item) or ismethod(item)] def _trigger_request(instance, request): """ Triggers request mock definition methods dynamically based on input keyword arguments passed to `pook.Mock` constructor. This is used to provide a more Pythonic interface vs chainable API approach. """ if not isinstance(request, Request): raise TypeError('request must be instance of pook.Request') # Register request matchers for key in request.keys: if hasattr(instance, key): getattr(instance, key)(getattr(request, key)) class Mock(object): """ Mock is used to declare and compose the HTTP request/response mock definition and matching expectations, which provides fluent API DSL. Arguments: url (str): URL to match. E.g: ``server.com/api?foo=bar``. method (str): HTTP method name to match. E.g: ``GET``. path (str): URL path to match. E.g: ``/api/users``. headers (dict): Header values to match. E.g: ``{'server': 'nginx'}``. header_present (str): Matches is a header is present. headers_present (list|tuple): Matches if multiple headers are present. type (str): Matches MIME ``Content-Type`` header. E.g: ``json``, ``xml``, ``html``, ``text/plain`` content (str): Same as ``type`` argument. params (dict): Matches the given URL params. param_exists (str): Matches if a given URL param exists. params_exists (list|tuple): Matches if a given URL params exists. body (str|regex): Matches the payload body by regex or strict comparison. json (dict|list|str|regex): Matches the payload body against the given JSON or regular expression. jsonschema (dict|str): Matches the payload body against the given JSONSchema. xml (str|regex): matches the payload body against the given XML string or regular expression. file (str): Disk file path to load body from. Analog to ``body`` param. times (int): Mock TTL or maximum number of times that the mock can be matched. persist (bool): Enable persistent mode. Mock won't be flushed even if it matched one or multiple times. delay (int): Optional network delay simulation (only applicable when using ``aiohttp`` HTTP client). callback (function): optional callback function called every time the mock is matched. reply (int): Mock response status. Defaults to ``200``. response_status (int): Mock response status. Alias to ``reply`` param. response_headers (dict): Response headers to use. response_type (str): Response MIME type expression or alias. Analog to ``type`` param. E.g: ``json``, ``xml``, ``text/plain``. response_body (str): Response body to use. response_json (dict|list|str): Response JSON to use. If Python is passed, it will be serialized as JSON transparently. response_xml (str): XML body string to use. request (pook.Request): Optional. Request mock definition object. response (pook.Response): Optional. Response mock definition object. Returns: pook.Mock """ def __init__(self, request=None, response=None, **kw): # Stores the number of times the mock should live self._times = 1 # Stores the number of times the mock has been matched self._matches = 0 # Stores the simulated error exception self._error = None # Stores the optional network delay in milliseconds self._delay = 0 # Stores the mock persistance mode. `True` means it will live forever self._persist = False # Optional binded engine where the mock belongs to self._engine = None # Store request-response mock matched calls self._calls = [] # Stores the input request instance self._request = request or Request() # Stores the response mock instance self._response = response or Response() # Stores the mock matcher engine used for outgoing traffic matching self.matchers = MatcherEngine() # Stores filters used to filter outgoing HTTP requests. self.filters = [] # Stores HTTP request mappers used by the mock. self.mappers = [] # Stores callback functions that will be triggered if the mock # matches outgoing traffic. self.callbacks = [] # Triggers instance methods based on argument names trigger_methods(self, kw) # Trigger matchers based on predefined request object, if needed if request: _trigger_request(self, request) def url(self, url): """ Defines the mock URL to match. It can be a full URL with path and query params. Protocol schema is optional, defaults to ``http://``. Arguments: url (str): mock URL to match. E.g: ``server.com/api``. Returns: self: current Mock instance. """ self._request.url = url self.add_matcher(matcher('URLMatcher', url)) return self def method(self, method): """ Defines the HTTP method to match. Use ``*`` to match any method. Arguments: method (str): method value to match. E.g: ``GET``. Returns: self: current Mock instance. """ self._request.method = method self.add_matcher(matcher('MethodMatcher', method)) return self def path(self, path): """ Defines a URL path to match. Only call this method if the URL has no path already defined. Arguments: path (str): URL path value to match. E.g: ``/api/users``. Returns: self: current Mock instance. """ url = furl(self._request.rawurl) url.path = path self._request.url = url.url self.add_matcher(matcher('PathMatcher', path)) return self def header(self, name, value): """ Defines a URL path to match. Only call this method if the URL has no path already defined. Arguments: path (str): URL path value to match. E.g: ``/api/users``. Returns: self: current Mock instance. """ headers = {name: value} self._request.headers = headers self.add_matcher(matcher('HeadersMatcher', headers)) return self def headers(self, headers=None, **kw): """ Defines a dictionary of arguments. Header keys are case insensitive. Arguments: headers (dict): headers to match. **headers (dict): headers to match as variadic keyword arguments. Returns: self: current Mock instance. """ headers = kw if kw else headers self._request.headers = headers self.add_matcher(matcher('HeadersMatcher', headers)) return self def header_present(self, *names): """ Defines a new header matcher expectation that must be present in the outgoing request in order to be satisfied, no matter what value it hosts. Header keys are case insensitive. Arguments: *names (str): header or headers names to match. Returns: self: current Mock instance. Example:: (pook.get('server.com/api') .header_present('content-type')) """ for name in names: headers = {name: re.compile('(.*)')} self.add_matcher(matcher('HeadersMatcher', headers)) return self def headers_present(self, headers): """ Defines a list of headers that must be present in the outgoing request in order to satisfy the matcher, no matter what value the headers hosts. Header keys are case insensitive. Arguments: headers (list|tuple): header keys to match. Returns: self: current Mock instance. Example:: (pook.get('server.com/api') .headers_present(['content-type', 'Authorization'])) """ headers = {name: re.compile('(.*)') for name in headers} self.add_matcher(matcher('HeadersMatcher', headers)) return self def type(self, value): """ Defines the request ``Content-Type`` header to match. You can pass one of the following aliases instead of the full MIME type representation: - ``json`` = ``application/json`` - ``xml`` = ``application/xml`` - ``html`` = ``text/html`` - ``text`` = ``text/plain`` - ``urlencoded`` = ``application/x-www-form-urlencoded`` - ``form`` = ``application/x-www-form-urlencoded`` - ``form-data`` = ``application/x-www-form-urlencoded`` Arguments: value (str): type alias or header value to match. Returns: self: current Mock instance. """ self.content(value) return self def content(self, value): """ Defines the ``Content-Type`` outgoing header value to match. You can pass one of the following type aliases instead of the full MIME type representation: - ``json`` = ``application/json`` - ``xml`` = ``application/xml`` - ``html`` = ``text/html`` - ``text`` = ``text/plain`` - ``urlencoded`` = ``application/x-www-form-urlencoded`` - ``form`` = ``application/x-www-form-urlencoded`` - ``form-data`` = ``application/x-www-form-urlencoded`` Arguments: value (str): type alias or header value to match. Returns: self: current Mock instance. """ header = {'Content-Type': TYPES.get(value, value)} self._request.headers = header self.add_matcher(matcher('HeadersMatcher', header)) return self def param(self, name, value): """ Defines an URL param key and value to match. Arguments: name (str): param name value to match. value (str): param name value to match. Returns: self: current Mock instance. """ self.params({name: value}) return self def param_exists(self, name): """ Checks if a given URL param name is present in the URL. Arguments: name (str): param name to check existence. Returns: self: current Mock instance. """ self.params({name: re.compile('(.*)')}) return self def params(self, params): """ Defines a set of URL query params to match. Arguments: params (dict): set of params to match. Returns: self: current Mock instance. """ url = furl(self._request.rawurl) url = url.add(params) self._request.url = url.url self.add_matcher(matcher('QueryMatcher', params)) return self def body(self, body): """ Defines the body data to match. ``body`` argument can be a ``str``, ``binary`` or a regular expression. Arguments: body (str|binary|regex): body data to match. Returns: self: current Mock instance. """ self._request.body = body self.add_matcher(matcher('BodyMatcher', body)) return self def json(self, json): """ Defines the JSON body to match. ``json`` argument can be an JSON string, a JSON serializable Python structure, such as a ``dict`` or ``list`` or it can be a regular expression used to match the body. Arguments: json (str|dict|list|regex): body JSON to match. Returns: self: current Mock instance. """ self._request.json = json self.add_matcher(matcher('JSONMatcher', json)) return self def jsonschema(self, schema): """ Defines a JSONSchema representation to be used for body matching. Arguments: schema (str|dict): dict or JSONSchema string to use. Returns: self: current Mock instance. """ self.add_matcher(matcher('JSONSchemaMatcher', schema)) return self def xml(self, xml): """ Defines a XML body value to match. Arguments: xml (str|regex): body XML to match. Returns: self: current Mock instance. """ self._request.xml = xml self.add_matcher(matcher('XMLMatcher', xml)) return self def file(self, path): """ Reads the body to match from a disk file. Arguments: path (str): relative or absolute path to file to read from. Returns: self: current Mock instance. """ with open(path, 'r') as f: self.body(str(f.read())) return self def add_matcher(self, matcher): """ Adds one or multiple custom matchers instances. Matchers must implement the following interface: - ``.__init__(expectation)`` - ``.match(request)`` - ``.name = str`` Matchers can optionally inherit from ``pook.matchers.BaseMatcher``. Arguments: *matchers (pook.matchers.BaseMatcher): matchers to add. Returns: self: current Mock instance. """ self.matchers.add(matcher) return self def use(self, *matchers): """ Adds one or multiple custom matchers instances. Matchers must implement the following interface: - ``.__init__(expectation)`` - ``.match(request)`` - ``.name = str`` Matchers can optionally inherit from ``pook.matchers.BaseMatcher``. Arguments: *matchers (pook.matchers.BaseMatcher): matchers to add. Returns: self: current Mock instance. """ [self.add_matcher(matcher) for matcher in matchers] return self def times(self, times=1): """ Defines the TTL limit for the current mock. The TTL number will determine the maximum number of times that the current mock can be matched and therefore consumed. Arguments: times (int): TTL number. Defaults to ``1``. Returns: self: current Mock instance. """ self._times = times return self def persist(self, status=None): """ Enables persistent mode for the current mock. Returns: self: current Mock instance. """ self._persist = status if type(status) is bool else True return self def filter(self, *filters): """ Registers one o multiple request filters used during the matching phase. Arguments: *mappers (function): variadic mapper functions. Returns: self: current Mock instance. """ _append_funcs(self.filters, filters) return self def map(self, *mappers): """ Registers one o multiple request mappers used during the mapping phase. Arguments: *mappers (function): variadic mapper functions. Returns: self: current Mock instance. """ _append_funcs(self.mappers, mappers) return self def callback(self, *callbacks): """ Registers one or multiple callback that will be called every time the current mock matches an outgoing HTTP request. Arguments: *callbacks (function): callback functions to call. Returns: self: current Mock instance. """ _append_funcs(self.callbacks, callbacks) return self def delay(self, delay=1000): """ Delay network response with certain milliseconds. Only supported by asynchronous HTTP clients, such as ``aiohttp``. Arguments: delay (int): milliseconds to delay response. Returns: self: current Mock instance. """ self._delay = int(delay) return self def error(self, error): """ Defines a simulated exception error that will be raised. Arguments: error (str|Exception): error to raise. Returns: self: current Mock instance. """ self._error = RuntimeError(error) if isinstance(error, str) else error return self def reply(self, status=200, new_response=False, **kw): """ Defines the mock response. Arguments: status (int, optional): response status code. Defaults to ``200``. **kw (dict): optional keyword arguments passed to ``pook.Response`` constructor. Returns: pook.Response: mock response definition instance. """ # Use or create a Response mock instance res = Response(**kw) if new_response else self._response # Define HTTP mandatory response status res.status(status or res._status) # Expose current mock instance in response for self-reference res.mock = self # Define mock response self._response = res # Return response return res def status(self, code=200): """ Defines the response status code. Equivalent to ``self.reply(code)``. Arguments: code (int): response status code. Defaults to ``200``. Returns: pook.Response: mock response definition instance. """ return self.reply(status=code) def response(self, status=200, **kw): """ Defines the mock response. Alias to ``.reply()`` Arguments: status (int): response status code. Defaults to ``200``. **kw (dict): optional keyword arguments passed to ``pook.Response`` constructor. Returns: pook.Response: mock response definition instance. """ return self.reply(status=status, **kw) def isdone(self): """ Returns ``True`` if the mock has been matched by outgoing HTTP traffic. Returns: bool: ``True`` if the mock was matched succesfully. """ return (self._persist and self._matches > 0) or self._times <= 0 def ismatched(self): """ Returns ``True`` if the mock has been matched at least once time. Returns: bool """ return self._matches > 0 @property def done(self): """ Attribute accessor that would be ``True`` if the current mock is done, and therefore have been matched multiple times. Returns: bool """ return self.isdone() @property def matched(self): """ Accessor property that would be ``True`` if the current mock have been matched at least once. See ``Mock.total_matches`` for more information. Returns: bool """ return self._matches > 0 @property def total_matches(self): """ Accessor property to retrieve the total number of times that the current mock has been matched. Returns: int """ return self._matches @property def matches(self): """ Accessor to retrieve the mock match calls registry. Returns: list[MockCall] """ return self._calls @property def calls(self): """ Accessor to retrieve the amount of mock matched calls. Returns: int """ return len(self.matches) def match(self, request): """ Matches an outgoing HTTP request against the current mock matchers. This method acts like a delegator to `pook.MatcherEngine`. Arguments: request (pook.Request): request instance to match. Raises: Exception: if the mock has an exception defined. Returns: tuple(bool, list[Exception]): ``True`` if the mock matches the outgoing HTTP request, otherwise ``False``. Also returns an optional list of error exceptions. """ # If mock already expired, fail it if self._times <= 0: raise PookExpiredMock('Mock expired') # Trigger mock filters for test in self.filters: if not test(request, self): return False, [] # Trigger mock mappers for mapper in self.mappers: request = mapper(request, self) if not request: raise ValueError('map function must return a request object') # Match incoming request against registered mock matchers matches, errors = self.matchers.match(request) # If not matched, return False if not matches: return False, errors # Register matched request for further inspecion and reference self._calls.append(request) # Increase mock call counter self._matches += 1 if not self._persist: self._times -= 1 # Raise simulated error if self._error: raise self._error # Trigger callback when matched for callback in self.callbacks: callback(request, self) return True, [] def __call__(self, fn): """ Overload Mock instance as callable object in order to be used as decorator definition syntax. Arguments: fn (function): function to decorate. Returns: function or pook.Mock """ # Support chain sequences of mock definitions if isinstance(fn, Response): return fn.mock if isinstance(fn, Mock): return fn # Force type assertion and raise an error if it is not a function if not isfunction(fn) and not ismethod(fn): raise TypeError('first argument must be a method or function') # Remove mock to prevent decorator definition scope collision self._engine.remove_mock(self) @functools.wraps(fn) def decorator(*args, **kw): # Re-register mock on decorator call self._engine.add_mock(self) # Force engine activation, if available # This prevents state issue while declaring mocks as decorators. # This might be removed in the future. engine_active = self._engine.active if not engine_active: self._engine.activate() # Call decorated target function try: return fn(*args, **kw) finally: # Finally remove mock after function execution # to prevent shared state self._engine.remove_mock(self) # If the engine was not previously active, disable it if not engine_active: self._engine.disable() return decorator def __repr__(self): """ Returns an human friendly readable instance data representation. Returns: str """ keys = ('matches', 'times', 'persist', 'matchers', 'response') args = [] for key in keys: if key == 'matchers': value = repr(self.matchers).replace('\n ', '\n ') value = value[:-2] + ' ])' elif key == 'response': value = repr(self._response) value = value[:-1] + ' )' else: value = repr(getattr(self, '_' + key)) args.append('{}={}'.format(key, value)) args = '(\n {}\n)'.format(',\n '.join(args)) return type(self).__name__ + args def __enter__(self): """ Implements context manager enter interface. """ # Make mock persistent if using default times if self._times == 1: self._persist = True # Automatically enable the mock engine, if needed if not self._engine.active: self._engine.activate() self._disable_engine = True return self def __exit__(self, etype, value, traceback): """ Implements context manager exit interface. """ # Force disable mock self._times = 0 # Automatically disable the mock engine, if needed if getattr(self, '_disable_engine', False): self._disable_engine = False self._engine.disable() if etype is not None: raise value pook-1.1.1/pook/mock_engine.py000066400000000000000000000070441435776471000163050ustar00rootroot00000000000000from .interceptors import interceptors class MockEngine(object): """ ``MockEngine`` represents the low-level mocking engine abstraction layer between ``pook`` and the underlying mocking mechanism responsible of intercepting and trigger outgoing HTTP traffic within the Python runtime. ``MockEngine`` implements the built-in `pook` mock engine based on HTTP interceptors strategy. Developers can implement and plug in their own ``MockEngine`` in order to fit custom mocking logic needs. You can see a custom ``MockEngine`` implementation here: http://bit.ly/2EymMro Custom mock engines must implementent at least the following methods: - `engine.__init__(self, engine)` - `engine.activate(self)` - `engine.disable(self)` Custom mock engines can optionally implement the following methods: - `engine.add_interceptors(self, *interceptors)` - `engine.flush_interceptors(self)` - `engine.disable_interceptor(self, name) -> bool` Arguments: engine (pook.Engine): injected pook engine to be used. Attributes: engine (pook.Engine): stores pook engine to be used. interceptors (list[pook.BaseInterceptor]): stores engine-level HTTP traffic interceptors. """ def __init__(self, engine): # Store pook engine self.engine = engine # Store HTTP client interceptors self.interceptors = [] # Self-register built-in interceptors self.add_interceptor(*interceptors) def add_interceptor(self, *interceptors): """ Adds one or multiple HTTP traffic interceptors to the current mocking engine. Interceptors are typically HTTP client specific wrapper classes that implements the pook interceptor interface. Arguments: interceptors (pook.interceptors.BaseInterceptor) """ for interceptor in interceptors: self.interceptors.append(interceptor(self.engine)) def flush_interceptors(self): """ Flushes registered interceptors in the current mocking engine. This method is low-level. Only call it if you know what you are doing. """ self.interceptors = [] def remove_interceptor(self, name): """ Removes a specific interceptor by name. Arguments: name (str): interceptor name to disable. Returns: bool: `True` if the interceptor was disabled, otherwise `False`. """ for index, interceptor in enumerate(self.interceptors): matches = ( type(interceptor).__name__ == name or getattr(interceptor, 'name') == name ) if matches: self.interceptors.pop(index) return True return False def activate(self): """ Activates the registered interceptors in the mocking engine. This means any HTTP traffic captures by those interceptors will trigger the HTTP mock matching engine in order to determine if a given HTTP transaction should be mocked out or not. """ [interceptor.activate() for interceptor in self.interceptors] def disable(self): """ Disables interceptors and stops intercepting any outgoing HTTP traffic. """ # Restore HTTP interceptors for interceptor in self.interceptors: try: interceptor.disable() except RuntimeError: pass # explicitely ignore runtime patch errors pook-1.1.1/pook/regex.py000066400000000000000000000022411435776471000151330ustar00rootroot00000000000000import re # Little hack to extra the regexp object type at runtime retype = type(re.compile('')) def isregex_expr(expr): """ Returns ``True`` is the given expression value is a regular expression like string with prefix ``re/`` and suffix ``/``, otherwise ``False``. Arguments: expr (mixed): expression value to test. Returns: bool """ if not isinstance(expr, str): return False return all([ len(expr) > 3, expr.startswith('re/'), expr.endswith('/') ]) def isregex(value): """ Returns ``True`` if the input argument object is a native regular expression object, otherwise ``False``. Arguments: value (mixed): input value to test. Returns: bool """ if not value: return False return any((isregex_expr(value), isinstance(value, retype))) def strip_regex(expr): """ Strips regular expression notation syntax characters from the given string expression. Arguments: expr (str): regular expression expression to strip Returns: str """ return expr.replace[3:-1] if isregex_expr(expr) else expr pook-1.1.1/pook/request.py000066400000000000000000000110211435776471000155050ustar00rootroot00000000000000import json as _json from .regex import isregex from .headers import HTTPHeaderDict from .helpers import trigger_methods from .matchers.url import protoregex from urllib.parse import urlparse, parse_qs, urlunparse class Request(object): """ Request object representing the request mock expectation DSL. Arguments: method (str): HTTP method to match. Defaults to ``GET``. url (str): URL request to intercept and match. headers (dict): HTTP headers to match. query (dict): URL query params to match. Complementely to URL defined query params. body (str|regex): request body payload to match. json (str|dict|list): JSON payload body structure to match. xml (str): XML payload data structure to match. Attributes: method (str): HTTP method to match. Defaults to ``GET``. url (str): URL request to intercept and match. headers (dict): HTTP headers to match. query (dict): URL query params to match. Complementely to URL defined query params. body (str|regex): request body payload to match. json (str|dict|list): JSON payload body structure to match. xml (str): XML payload data structure to match. """ # Store keys keys = ('method', 'headers', 'body', 'url', 'query') def __init__(self, method='GET', **kw): self._url = None self._body = None self._query = None self._method = method self._extra = kw.get('extra') self._headers = HTTPHeaderDict() trigger_methods(self, kw) @property def method(self): return self._method @method.setter def method(self, method): self._method = method @property def headers(self): return self._headers @headers.setter def headers(self, headers): if not hasattr(headers, '__setitem__'): raise TypeError('headers must be a dictionary') self._headers.extend(headers) @property def extra(self): return self._extra @extra.setter def extra(self, extra): if not isinstance(extra, dict): raise TypeError('extra must be a dictionary') self._extra = extra @property def url(self): return self._url @property def rawurl(self): return self._url if isregex(self._url) else urlunparse(self._url) @url.setter def url(self, url): if isregex(url): self._url = url else: if not protoregex.match(url): url = 'http://{}'.format(url) self._url = urlparse(url) self._query = (parse_qs(self._url.query) if self._url.query else self._query) @property def query(self, url): return self._query @query.setter def query(self, params): self._query = parse_qs(params) @property def body(self): return self._body @body.setter def body(self, body): if hasattr(body, 'decode'): try: body = body.decode('utf-8', 'strict') except Exception: pass self._body = body @property def json(self): return _json.loads(self._body) @json.setter def json(self, data): if isinstance(data, str): self._body = data else: self._body = _json.dumps(data) @property def xml(self): return self._body @xml.setter def xml(self, data): self._body = data def copy(self): """ Copies the current Request object instance for side-effects purposes. Returns: pook.Request: copy of the current Request instance. """ req = type(self)() req.__dict__ = self.__dict__.copy() req._headers = self.headers.copy() return req def __repr__(self): """ Returns an human friendly readable instance data representation. Returns: str """ entries = [] entries.append('Method: {}'.format(self._method)) entries.append('URL: {}'.format( self._url if isregex(self._url) else self.rawurl)) if self._query: entries.append('Query: {}'.format(self._query)) if self._headers: entries.append('Headers: {}'.format(self._headers)) if self._body: entries.append('Body: {}'.format(self._body)) separator = '=' * 50 return (separator + '\n{}\n' + separator).format('\n'.join(entries)) pook-1.1.1/pook/response.py000066400000000000000000000143321435776471000156630ustar00rootroot00000000000000import json from .headers import HTTPHeaderDict from .helpers import trigger_methods from .constants import TYPES class Response(object): """ Response is used to declare and compose an HTTP mock responses fields. It provides a chainable DSL interface for easier and declarative usage. Arguments: status (int): HTTP response status code. Defaults to ``200``. headers (dict): HTTP response headers. body (str|bytes): HTTP response body. json (str|dict|list): HTTP response JSON body. xml (str): HTTP response XML body. type (str): HTTP response content MIME type. file (str): file path to HTTP body response. Attributes: mock (pook.Mock): reference to mock instance. """ def __init__(self, **kw): self._status = 200 self._mock = None self._body = None self._headers = HTTPHeaderDict() # Trigger response method based on input arguments trigger_methods(self, kw) def status(self, code=200): """ Defines the response status code. Arguments: code (int): response status code. Returns: self: ``pook.Response`` current instance. """ self._status = int(code) return self def header(self, key, value): """ Defines a new response header. Alias to ``Response.header()``. Arguments: header (str): header name. value (str): header value. Returns: self: ``pook.Response`` current instance. """ if type(key) is tuple: key, value = str(key[0]), key[1] headers = {key: value} self._headers.extend(headers) return self def headers(self, headers): """ Defines a new response header. Alias to ``Response.header()``. Arguments: header (str): header name. value (str): header value. Returns: self: ``pook.Response`` current instance. """ self._headers.extend(headers) return self def set(self, header, value): """ Defines a new response header. Alias to ``Response.header()``. Arguments: header (str): header name. value (str): header value. Returns: self: ``pook.Response`` current instance. """ self._headers[header] = value return self def type(self, name): """ Defines the response ``Content-Type`` header. Alias to ``Response.content(mime)``. You can pass one of the following type aliases instead of the full MIME type representation: - ``json`` = ``application/json`` - ``xml`` = ``application/xml`` - ``html`` = ``text/html`` - ``text`` = ``text/plain`` - ``urlencoded`` = ``application/x-www-form-urlencoded`` - ``form`` = ``application/x-www-form-urlencoded`` - ``form-data`` = ``application/x-www-form-urlencoded`` Arguments: value (str): type alias or header value to match. Returns: self: ``pook.Response`` current instance. """ self.content(name) return self def content(self, name): """ Defines the response ``Content-Type`` header. You can pass one of the following type aliases instead of the full MIME type representation: - ``json`` = ``application/json`` - ``xml`` = ``application/xml`` - ``html`` = ``text/html`` - ``text`` = ``text/plain`` - ``urlencoded`` = ``application/x-www-form-urlencoded`` - ``form`` = ``application/x-www-form-urlencoded`` - ``form-data`` = ``application/x-www-form-urlencoded`` Arguments: value (str): type alias or header value to match. Returns: self: ``pook.Response`` current instance. """ self._headers['Content-Type'] = TYPES.get(name, name) return self def body(self, body, chunked=False): """ Defines response body data. Arguments: body (str|bytes|list): response body to use. chunked (bool): return a chunked response. Returns: self: ``pook.Response`` current instance. """ if isinstance(body, bytes): body = body.decode('utf-8') self._body = body if chunked: self.header('Transfer-Encoding', 'chunked') return self def json(self, data): """ Defines the mock response JSON body. Arguments: data (dict|list|str): JSON body data. Returns: self: ``pook.Response`` current instance. """ self._headers['Content-Type'] = 'application/json' if not isinstance(data, str): data = json.dumps(data, indent=4) self._body = data return self def xml(self, xml): """ Defines the mock response XML body. For not it only supports ``str`` as input type. Arguments: xml (str): XML body data to use. Returns: self: ``pook.Response`` current instance. """ self.body(xml) return self def file(self, path): """ Defines the response body from file contents. Arguments: path (str): disk file path to load. Returns: self: ``pook.Response`` current instance. """ with open(path, 'r') as f: self._body = str(f.read()) return self @property def mock(self): """ Getter accessor for `mock` attribute. """ return self._mock @mock.setter def mock(self, mock): """ Setter for ``mock`` attribute. """ self._mock = mock def __repr__(self): """ Returns an human friendly readable instance data representation. Returns: str """ args = [] for key in ('headers', 'status', 'body'): value = getattr(self, '_{}'.format(key)) args.append('{}={}'.format(key, value)) return 'Response(\n {}\n)'.format(',\n '.join(args)) pook-1.1.1/requirements-dev.txt000066400000000000000000000003611435776471000165400ustar00rootroot00000000000000flake8 wheel>=0.29 coveralls>=1.1 pytest~=7.2.0 pytest-cov~=4.0.0 pytest-flakes~=1.0.1 nose~=1.3.7 Sphinx~=1.4.8 sphinx-rtd-theme~=0.1.9 requests>=2.20.0 urllib3>=1.24.2 bumpversion~=0.5.3 aiohttp~=3.8.3 mocket~=1.6.0 pytest-asyncio~=0.20.3 pook-1.1.1/requirements.txt000066400000000000000000000000601435776471000157600ustar00rootroot00000000000000jsonschema>=2.5.1 xmltodict>=0.11.0 furl>=0.5.6 pook-1.1.1/setup.py000066400000000000000000000062271435776471000142210ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ pook ==== Versatile HTTP traffic mocking and expectations made easy in Python. :copyright: (c) 2016-2020 Tomas Aparicio :license: MIT """ import os import sys import codecs from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand # Publish command if sys.argv[-1] == 'publish': os.system('python setup.py sdist upload') sys.exit() setup_requires = [] if 'test' in sys.argv: setup_requires.append('pytest') def read_version(package): init_path = os.path.join(package, '__init__.py') with open(init_path, 'r') as fd: for line in fd: if line.startswith('__version__ = '): return line.split()[-1].strip().strip("'") # Get package current version version = read_version('pook') class PyTest(TestCommand): def finalize_options(self): TestCommand.finalize_options(self) self.test_args = ['tests/'] self.test_suite = True def run_tests(self): # import here, cause outside the eggs aren't loaded import pytest errno = pytest.main(self.test_args) sys.exit(errno) with codecs.open('requirements-dev.txt', encoding='utf-8') as f: tests_require = f.read().splitlines() with codecs.open('requirements.txt', encoding='utf-8') as f: install_requires = f.read().splitlines() with codecs.open('README.rst', encoding='utf-8') as f: readme = f.read() with codecs.open('History.rst', encoding='utf-8') as f: history = f.read() setup( name='pook', version=version, author='Tomas Aparicio', author_email='tomas@aparicio.me', description=( 'HTTP traffic mocking and expectations made easy' ), url='https://github.com/h2non/pook', license='MIT', long_description=readme + '\n\n' + history, long_description_content_type='text/x-rst', py_modules=['pook'], zip_safe=False, install_requires=install_requires, tests_require=tests_require, packages=find_packages(exclude=['tests', 'examples', 'docs']), package_data={'': [ 'LICENSE', 'README.rst', 'History.rst', 'requirements.txt', 'requirements-dev.txt' ]}, package_dir={'pook': 'pook'}, include_package_data=True, cmdclass={'test': PyTest}, classifiers=[ 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Operating System :: OS Independent', 'Development Status :: 5 - Production/Stable', 'Natural Language :: English', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Topic :: Software Development', 'Topic :: Software Development :: Libraries :: Python Modules', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy' ], ) pook-1.1.1/tests/000077500000000000000000000000001435776471000136425ustar00rootroot00000000000000pook-1.1.1/tests/__init__.py000066400000000000000000000000001435776471000157410ustar00rootroot00000000000000pook-1.1.1/tests/integration/000077500000000000000000000000001435776471000161655ustar00rootroot00000000000000pook-1.1.1/tests/integration/__init__.py000066400000000000000000000000001435776471000202640ustar00rootroot00000000000000pook-1.1.1/tests/integration/engines/000077500000000000000000000000001435776471000176155ustar00rootroot00000000000000pook-1.1.1/tests/integration/engines/__init__.py000066400000000000000000000000001435776471000217140ustar00rootroot00000000000000pook-1.1.1/tests/integration/engines/nose_suite.py000066400000000000000000000016661435776471000223550ustar00rootroot00000000000000# -*- coding: utf-8 -*- import pook import requests @pook.activate def test_simple_pook_request(): pook.get('server.com/foo').reply(204) res = requests.get('http://server.com/foo') assert res.status_code == 204 @pook.on def test_enable_engine(): pook.get('server.com/foo').reply(204) res = requests.get('http://server.com/foo') assert res.status_code == 204 @pook.get('server.com/foo', reply=204) def test_decorator(): res = requests.get('http://server.com/foo') assert res.status_code == 204 def test_context_manager(): with pook.use(): pook.get('server.com/bar', reply=204) res = requests.get('http://server.com/bar') assert res.status_code == 204 @pook.on def test_no_match_exception(): pook.get('server.com/bar', reply=204) try: requests.get('http://server.com/baz') except Exception: pass else: raise RuntimeError('expected to fail') pook-1.1.1/tests/integration/engines/pytest_suite.py000066400000000000000000000016211435776471000227300ustar00rootroot00000000000000# -*- coding: utf-8 -*- import pook import pytest import requests @pook.on def test_simple_pook_request(): pook.get('httpbin.org/foo').reply(204) res = requests.get('http://httpbin.org/foo') assert res.status_code == 204 @pook.on def test_enable_engine(): pook.get('server.com/foo').reply(204) res = requests.get('http://server.com/foo') assert res.status_code == 204 pook.disable() @pook.get('server.com/bar', reply=204) def test_decorator(): res = requests.get('http://server.com/bar') assert res.status_code == 204 def test_context_manager(): with pook.use(): pook.get('server.com/baz', reply=204) res = requests.get('http://server.com/baz') assert res.status_code == 204 @pook.on def test_no_match_exception(): pook.get('server.com/bar', reply=204) with pytest.raises(Exception): requests.get('http://server.com/baz') pook-1.1.1/tests/integration/engines/unittest_suite.py000066400000000000000000000022221435776471000232550ustar00rootroot00000000000000# -*- coding: utf-8 -*- import unittest import requests import pook class TestUnitTestEngine(unittest.TestCase): @pook.on def test_simple_pook_request(self): pook.get('server.com/foo').reply(204) res = requests.get('http://server.com/foo') self.assertEqual(res.status_code, 204) @pook.on def test_enable_engine(self): pook.get('server.com/foo').reply(204) res = requests.get('http://server.com/foo') self.assertEqual(res.status_code, 204) @pook.get('server.com/foo', reply=204) def test_decorator(self): res = requests.get('http://server.com/foo') self.assertEqual(res.status_code, 204) def test_context_manager(self): with pook.use(): pook.get('server.com/bar', reply=204) res = requests.get('http://server.com/bar') self.assertEqual(res.status_code, 204) @pook.on def test_no_match_exception(self): pook.get('server.com/bar', reply=204) try: requests.get('http://server.com/baz') except Exception: pass else: raise RuntimeError('expected to fail') pook-1.1.1/tests/integration/engines_test.py000066400000000000000000000007741435776471000212360ustar00rootroot00000000000000# -*- coding: utf-8 -*- import subprocess # List of engine specific test commands to run engine_tests = ( 'py.test tests/integration/engines/pytest_suite.py', # 'nosetests tests/integration/engines/nose_suite.py', 'python -m unittest tests.integration.engines.unittest_suite', ) def test_engines(): for cmd in engine_tests: args = cmd.split(' ') code = subprocess.call(args) if code != 0: raise AssertionError('invalid exit code for: {}'.format(cmd)) pook-1.1.1/tests/integration/examples_test.py000066400000000000000000000011671435776471000214210ustar00rootroot00000000000000# -*- coding: utf-8 -*- import re import os import subprocess # Regular expression used to match Python files pyfile = re.compile('.py$') # List of allowed example to fail allowed_errors = ( 'simulated_error.py' ) def test_examples(): # List of file examples examples = [f for f in os.listdir('examples') if pyfile.match(f)] # Test file example for example in examples: code = subprocess.call(['python', 'examples/{}'.format(example)]) expected = 1 if example in allowed_errors else 0 if code != expected: raise AssertionError('invalid exit code: {}'.format(example)) pook-1.1.1/tests/integration/pook_requests_test.py000066400000000000000000000014251435776471000225030ustar00rootroot00000000000000# -*- coding: utf-8 -*- import pook import pytest import requests @pytest.fixture def mock(): pook.activate() yield pook pook.disable() def test_requests_get(mock): body = {'error': 'not found'} mock.get('http://foo.com').reply(404).json(body) res = requests.get('http://foo.com') assert res.status_code == 404 assert res.headers == {'Content-Type': 'application/json'} assert res.json() == body assert pook.isdone() is True def test_requests_match_url(mock): body = {'foo': 'bar'} mock.get('http://foo.com').reply(200).json(body) res = requests.get('http://foo.com') assert res.status_code == 200 assert res.headers == {'Content-Type': 'application/json'} assert res.json() == body assert pook.isdone() is True pook-1.1.1/tests/unit/000077500000000000000000000000001435776471000146215ustar00rootroot00000000000000pook-1.1.1/tests/unit/__init__.py000066400000000000000000000000001435776471000167200ustar00rootroot00000000000000pook-1.1.1/tests/unit/api_test.py000066400000000000000000000017161435776471000170100ustar00rootroot00000000000000# -*- coding: utf-8 -*- import pytest from pook import api @pytest.fixture def engine(): return api.engine() def test_engine(engine): assert engine == api._engine def test_activate(engine): assert engine.active is False api.activate() assert engine.active is True api.disable() assert engine.active is False def test_on(engine): assert engine.active is False api.on() assert engine.active is True api.off() assert engine.active is False def test_use(engine): assert engine.active is False with api.use() as engine: assert engine.active is True assert engine.active is True assert engine.active is False def test_mock_contructors(engine): assert engine.active is False assert engine.isdone() is True api.mock('foo.com') assert engine.isdone() is False assert len(engine.mocks) == 1 api.off() assert len(engine.mocks) == 0 assert engine.active is False pook-1.1.1/tests/unit/engine_test.py000066400000000000000000000007041435776471000175000ustar00rootroot00000000000000import pytest from pook import Engine @pytest.fixture def engine(): return Engine() def test_engine_use_network_filter(engine): assert len(engine.network_filters) == 0 engine.use_network_filter(lambda x: x) assert len(engine.network_filters) == 1 def test_engine_enable_network(engine): assert len(engine.network_filters) == 0 engine.enable_network('http://foo', 'http://bar') assert len(engine.network_filters) == 2 pook-1.1.1/tests/unit/exceptions_test.py000066400000000000000000000005601435776471000204140ustar00rootroot00000000000000# -*- coding: utf-8 -*- from pook import exceptions as ex def test_exceptions(): assert isinstance(ex.PookNoMatches(), Exception) assert isinstance(ex.PookInvalidBody(), Exception) assert isinstance(ex.PookExpiredMock(), Exception) assert isinstance(ex.PookNetworkFilterError(), Exception) assert isinstance(ex.PookInvalidArgument(), Exception) pook-1.1.1/tests/unit/interceptors/000077500000000000000000000000001435776471000173425ustar00rootroot00000000000000pook-1.1.1/tests/unit/interceptors/__init__.py000066400000000000000000000000001435776471000214410ustar00rootroot00000000000000pook-1.1.1/tests/unit/interceptors/aiohttp_test.py000066400000000000000000000014031435776471000224210ustar00rootroot00000000000000import aiohttp import pook import pytest pytestmark = pytest.mark.asyncio URL = "https://httpbin.org/status/404" def _pook_url(): return pook.head(URL).reply(200).mock async def test_async_with_request(): # Cannot use `@pook.on` with pytest marks pook.on() mock = _pook_url() async with aiohttp.ClientSession() as session: async with session.head(URL) as req: assert req.status == 200 pook.off() assert len(mock.matches) == 1 async def test_await_request(): # Cannot use `@pook.on` with pytest marks pook.on() mock = _pook_url() async with aiohttp.ClientSession() as session: req = await session.head(URL) assert req.status == 200 pook.off() assert len(mock.matches) == 1 pook-1.1.1/tests/unit/interceptors/module_test.py000066400000000000000000000003501435776471000222360ustar00rootroot00000000000000from pook import interceptors class CustomInterceptor(interceptors.BaseInterceptor): pass def test_add_custom_interceptor(): interceptors.add(CustomInterceptor) assert CustomInterceptor in interceptors.interceptors pook-1.1.1/tests/unit/interceptors/urllib3_test.py000066400000000000000000000022621435776471000223310ustar00rootroot00000000000000# -*- coding: utf-8 -*- import urllib3 import pook @pook.on def assert_chunked_response(input_data, expected): (pook.get('httpbin.org/foo') .reply(204) .body(input_data, chunked=True)) http = urllib3.PoolManager() r = http.request('GET', 'httpbin.org/foo') assert r.status == 204 chunks = list(r.read_chunked()) chunks = [c.decode() if isinstance(c, bytes) else c for c in chunks] assert chunks == expected def test_chunked_response_list(): assert_chunked_response(['a', 'b', 'c'], ['a', 'b', 'c']) def test_chunked_response_str(): assert_chunked_response('text', ['text']) def test_chunked_response_byte(): assert_chunked_response(b'byteman', ['byteman']) def test_chunked_response_empty(): assert_chunked_response('', []) def test_chunked_response_contains_newline(): assert_chunked_response('newline\r\n', ['newline\r\n']) def test_activate_disable(): original = urllib3.connectionpool.HTTPConnectionPool.urlopen interceptor = pook.interceptors.Urllib3Interceptor(pook.MockEngine) interceptor.activate() interceptor.disable() assert urllib3.connectionpool.HTTPConnectionPool.urlopen == original pook-1.1.1/tests/unit/matcher_test.py000066400000000000000000000000001435776471000176430ustar00rootroot00000000000000pook-1.1.1/tests/unit/matchers/000077500000000000000000000000001435776471000164275ustar00rootroot00000000000000pook-1.1.1/tests/unit/matchers/__init__.py000066400000000000000000000000001435776471000205260ustar00rootroot00000000000000pook-1.1.1/tests/unit/matchers/base_test.py000066400000000000000000000021771435776471000207610ustar00rootroot00000000000000# -*- coding: utf-8 -*- import pytest from pook.matchers.base import BaseMatcher class _BaseMatcher(BaseMatcher): def match(self, x): pass def test_base_matcher_instance(): matcher = _BaseMatcher('foo') assert matcher.name == '_BaseMatcher' assert matcher.negate is False assert matcher.expectation == 'foo' assert matcher.to_dict() == {'_BaseMatcher': 'foo'} assert matcher.__repr__() == '_BaseMatcher(foo)' assert matcher.__str__() == 'foo' def test_base_matcher_compare(): assert _BaseMatcher('foo').compare('foo', 'foo') assert _BaseMatcher('foo').compare('foo', 'foo') with pytest.raises(AssertionError): assert _BaseMatcher('foo').compare('foo', 'bar') def test_base_matcher_exceptions(): assert _BaseMatcher('foo').match(None) is None with pytest.raises(ValueError, match='expectation argument cannot be empty'): _BaseMatcher(None) def test_base_matcher_matcher(): assert BaseMatcher.matcher(lambda x: True)(BaseMatcher) matcher = _BaseMatcher('foo', negate=True) assert BaseMatcher.matcher(lambda x: False)(matcher) pook-1.1.1/tests/unit/matchers/body_test.py000066400000000000000000000000001435776471000207630ustar00rootroot00000000000000pook-1.1.1/tests/unit/matchers/headers_test.py000066400000000000000000000000001435776471000214410ustar00rootroot00000000000000pook-1.1.1/tests/unit/matchers/helpers_test.py000066400000000000000000000000001435776471000214700ustar00rootroot00000000000000pook-1.1.1/tests/unit/matchers/json_schema_test.py000066400000000000000000000000001435776471000223170ustar00rootroot00000000000000pook-1.1.1/tests/unit/matchers/json_test.py000066400000000000000000000000001435776471000207770ustar00rootroot00000000000000pook-1.1.1/tests/unit/matchers/method_test.py000066400000000000000000000000001435776471000213060ustar00rootroot00000000000000pook-1.1.1/tests/unit/matchers/path_test.py000066400000000000000000000000001435776471000207620ustar00rootroot00000000000000pook-1.1.1/tests/unit/matchers/query_test.py000066400000000000000000000000001435776471000211730ustar00rootroot00000000000000pook-1.1.1/tests/unit/matchers/url_test.py000066400000000000000000000047671435776471000206600ustar00rootroot00000000000000# -*- coding: utf-8 -*- # flake8: noqa import re import pytest from pook.request import Request from pook.matchers.url import URLMatcher def run_tests(cases, regex=False): for test in cases: match_url, url, matches = test req = Request(url=url) if regex: match_url = re.compile(match_url, re.IGNORECASE) if matches: assert URLMatcher(match_url).match(req) else: with pytest.raises(Exception): URLMatcher(match_url).match(req) def test_url_matcher_urlparse(): run_tests(( # Valid cases ('http://foo.com', 'http://foo.com', True), ('http://foo.com:80', 'http://foo.com:80', True), ('http://foo.com', 'http://foo.com/foo/bar', True), ('http://foo.com/foo', 'http://foo.com/foo', True), ('http://foo.com/foo/bar', 'http://foo.com/foo/bar', True), ('http://foo.com/foo/bar/baz', 'http://foo.com/foo/bar/baz', True), ('http://foo.com/foo?x=y&z=w', 'http://foo.com/foo?x=y&z=w', True), # Invalid cases ('http://foo.com', 'http://bar.com', False), ('http://foo.com:80', 'http://foo.com:443', False), ('http://foo.com/foo', 'http://foo.com', False), ('http://foo.com/foo', 'http://foo.com/bar', False), ('http://foo.com/foo/bar', 'http://foo.com/bar/foo', False), ('http://foo.com/foo/bar/baz', 'http://foo.com/baz/bar/foo', False), ('http://foo.com/foo?x=y&z=w', 'http://foo.com/foo?x=x&y=y', False), )) def test_url_matcher_regex(): run_tests(( # Valid cases ('http://foo.com', 'http://foo.com', True), ('http://foo.com:80', 'http://foo.com:80', True), ('^http://foo.com', 'http://foo.com/foo/bar', True), ('http://foo.com/foo', 'http://foo.com/foo', True), ('http://foo.com/foo/bar', 'http://foo.com/foo/bar', True), ('http://foo.com/foo/bar/baz', 'http://foo.com/foo/bar/baz', True), ('http://foo.com/foo\?x=[0-9]', 'http://foo.com/foo?x=5', True), # Invalid cases ('http://foo.com', 'http://bar.com', False), ('http://foo.com:80', 'http://foo.com:443', False), ('^http://foo.com$', 'http://foo.com/bar', False), ('http://foo.com/foo', 'http://foo.com/bar', False), ('http://foo.com/foo/bar', 'http://foo.com/bar/foo', False), ('http://foo.com/foo/bar/baz', 'http://foo.com/baz/bar/foo', False), ('http://foo.com/foo\?x=[1-3]', 'http://foo.com/foo?x=5', False), ), regex=True) pook-1.1.1/tests/unit/mock_engine_test.py000066400000000000000000000023541435776471000205140ustar00rootroot00000000000000# -*- coding: utf-8 -*- import pytest from pook import MockEngine, Engine from pook.interceptors import BaseInterceptor class Interceptor(BaseInterceptor): def activate(self): self.active = True def disable(self): self.active = False @pytest.fixture def engine(): return MockEngine(Engine()) def test_mock_engine_instance(engine): assert isinstance(engine.engine, Engine) assert isinstance(engine.interceptors, list) assert len(engine.interceptors) >= 2 def test_mock_engine_flush(engine): assert len(engine.interceptors) >= 2 engine.flush_interceptors() assert len(engine.interceptors) == 0 def test_mock_engine_interceptors(engine): engine.flush_interceptors() engine.add_interceptor(Interceptor) assert len(engine.interceptors) == 1 assert isinstance(engine.interceptors[0], Interceptor) engine.remove_interceptor('Interceptor') assert len(engine.interceptors) == 0 def test_mock_engine_status(engine): engine.flush_interceptors() engine.add_interceptor(Interceptor) assert len(engine.interceptors) == 1 interceptor = engine.interceptors[0] engine.activate() assert interceptor.active engine.disable() assert not interceptor.active pook-1.1.1/tests/unit/mock_test.py000066400000000000000000000005641435776471000171700ustar00rootroot00000000000000# -*- coding: utf-8 -*- import pytest from pook.mock import Mock @pytest.fixture def mock(): return Mock() def matcher(mock): return mock.matchers[0] def test_mock_url(mock): mock.url('http://google.es') assert str(matcher(mock)) == 'http://google.es' def test_new_response(mock): assert mock.reply() != mock.reply(new_response=True, json={}) pook-1.1.1/tests/unit/regex_test.py000066400000000000000000000015521435776471000173470ustar00rootroot00000000000000# -*- coding: utf-8 -*- import re from pook.regex import isregex_expr, isregex def test_isregex_expr(): cases = ( ('re/[a-z]/', True), ('re/[0-9]/', True), ('re/[(.*)]/', True), ('re//', True), ('RE/[0-9]/', False), ('/[0-9]/', False), ('[0-9]', False), ('//', False), ('re/', False), ('re/[0-1]/-', False), ('e/[0-1]/-', False), ('e/[0-1]/', False), ('', False), ([], False), (1, False), (None, False), ) for case in cases: assert isregex_expr(case[0]) is case[1] def test_isregex(): cases = ( (re.compile('[a-z]'), True), ('[a-z]', False), ('', False), ([], False), (1, False), (None, False), ) for case in cases: assert isregex(case[0]) is case[1] pook-1.1.1/tox.ini000066400000000000000000000003261435776471000140140ustar00rootroot00000000000000[tox] envlist = {py35,py36,py37,py38,py39,py310,py311,pypy} [testenv] setenv = PYTHONPATH = {toxinidir} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-dev.txt commands = make lint make test