diff --git a/.gitignore b/.gitignore index 7bbc71c..a665c85 100644 --- a/.gitignore +++ b/.gitignore @@ -62,7 +62,7 @@ instance/ .scrapy # Sphinx documentation -docs/_build/ +docs/build/ # PyBuilder target/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..27a504f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +dist: xenial +language: python +python: +- 3.6 +- 3.7 +script: +- make travis +after_success: +- codecov +deploy: + provider: pypi + password: + secure: XOyq2ZlY9OIwUEI4aJK/lG1CRDcd12XahLCAda5uUEODEB+hYpyr9zLGDHCLSIu/jU0jhbKbRyhIM5056TM8d29XLpq7pXTDMDwUxEc9rY37cC+OvO8MoocMAkzfvdQ8vaNMe3xrW49vGApMNm8Er+aZvMhqThfjjA/mIaDxjyajt+d0pN7Ow8IaGur8vPzvhaNZuS/bYiwhAHXEEUBIk6xLB1nPkk1FnVIXETjM1c28Sas9ki95gvgWWkMwtGXmn/T6waf0zOtdAC66lJZL5nwj1MTjw1uhZYB+/QhIlPzPSyvDD9sIqJnEEOlT5eUKGV2yp+Eo3qjN7fPIKmhB5ydtIvz/96why0Gobc6BIrOROYT1kn/NTi3R5FCZDQ4+xQPd6po+Pu8COqSRhSJWltAWgnNR1JmUnPVj0rEQaJzi/gqmGQ1Y4TQ0/KsjbYuU7p+mzpcGKh+y1fiPFYRTvrRlBeZbx38vJ8g0rpNtSGi2DkDNl2vCEl4ZMhKnO2AY9TSX9ziIpmPxCN6wo6aIhkmOPIu6QEWv6MVl5pHDIRnm6MhEZ6RL50FeN4jRCrE6ORi0xuAb02tBUAl25teKwiuQ771jcvdsE48+gySBVffHfuO+tdC0+9JPf96uwVemCh5eHxj7wyEFAR1e00NnqY8tGaGroeQ3VkeZxSu/irc= +user: bobthemighty +true: + tags: true + distributions: sdist + repo: bobthemighty/punq diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6243656 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +BLACK_EXCLUSION=docs/source/conf.py +default: init test +travis: init check_lint test + +init: + pip install pipenv + pipenv install --dev + +test: lint + pipenv run run-contexts -sv + +lint: + pipenv run black . --exclude "${BLACK_EXCLUSION}" + +check_lint: + pipenv run black --check . --exclude "${BLACK_EXCLUSION}" diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..19d0b3c --- /dev/null +++ b/Pipfile @@ -0,0 +1,22 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +colorama = "==0.3.9" +expects = "==0.8.0" +Contexts = "==0.11.2" +black = "*" +sphinx = "*" +setuptools-scm = "*" + +[packages] +setuptools-scm = "*" +punq = {editable = true,path = "."} + +[requires] +python_version = "3.7" + +[pipenv] +allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..9e1f7f5 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,262 @@ +{ + "_meta": { + "hash": { + "sha256": "1cc372e5d6815381868f3c4d8fc2b911a9f2149158453d8afcef0ea7e3323181" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "punq": { + "editable": true, + "path": "." + }, + "setuptools-scm": { + "hashes": [ + "sha256:057a67cb0a33e0f95edd828e47809f49b7104f4bc333a98fd35d4d05738c6187", + "sha256:52ab47715fa0fc7d8e6cd15168d1a69ba995feb1505131c3e814eb7087b57358" + ], + "index": "pypi", + "version": "==3.2.0" + } + }, + "develop": { + "alabaster": { + "hashes": [ + "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", + "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" + ], + "version": "==0.7.12" + }, + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, + "attrs": { + "hashes": [ + "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", + "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + ], + "version": "==18.2.0" + }, + "babel": { + "hashes": [ + "sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", + "sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23" + ], + "version": "==2.6.0" + }, + "black": { + "hashes": [ + "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", + "sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5" + ], + "index": "pypi", + "version": "==18.9b0" + }, + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "colorama": { + "hashes": [ + "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", + "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1" + ], + "index": "pypi", + "version": "==0.3.9" + }, + "contexts": { + "hashes": [ + "sha256:ed8ce1cf23b25008c14ea311cd2a75f17ccd855d041f7e0bede6950dd8035efc", + "sha256:f31ee753182f204d69355066ec29a3cd684741a9c94152366debbc11761f48ce" + ], + "index": "pypi", + "version": "==0.11.2" + }, + "docutils": { + "hashes": [ + "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", + "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", + "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + ], + "version": "==0.14" + }, + "expects": { + "hashes": [ + "sha256:37538d7b0fa9c0d53e37d07b0e8c07d89754d3deec1f0f8ed1be27f4f10363dd" + ], + "index": "pypi", + "version": "==0.8.0" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "imagesize": { + "hashes": [ + "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", + "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5" + ], + "version": "==1.1.0" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, + "markupsafe": { + "hashes": [ + "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", + "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", + "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", + "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", + "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", + "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", + "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", + "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", + "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", + "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", + "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", + "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", + "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", + "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", + "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", + "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", + "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", + "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", + "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", + "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", + "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", + "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", + "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", + "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", + "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", + "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", + "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", + "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" + ], + "version": "==1.1.0" + }, + "packaging": { + "hashes": [ + "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", + "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" + ], + "version": "==19.0" + }, + "pygments": { + "hashes": [ + "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", + "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" + ], + "version": "==2.3.1" + }, + "pyparsing": { + "hashes": [ + "sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a", + "sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3" + ], + "version": "==2.3.1" + }, + "pytz": { + "hashes": [ + "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", + "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c" + ], + "version": "==2018.9" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "version": "==2.21.0" + }, + "setuptools-scm": { + "hashes": [ + "sha256:057a67cb0a33e0f95edd828e47809f49b7104f4bc333a98fd35d4d05738c6187", + "sha256:52ab47715fa0fc7d8e6cd15168d1a69ba995feb1505131c3e814eb7087b57358" + ], + "index": "pypi", + "version": "==3.2.0" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "snowballstemmer": { + "hashes": [ + "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", + "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" + ], + "version": "==1.2.1" + }, + "sphinx": { + "hashes": [ + "sha256:b53904fa7cb4b06a39409a492b949193a1b68cc7241a1a8ce9974f86f0d24287", + "sha256:c1c00fc4f6e8b101a0d037065043460dffc2d507257f2f11acaed71fd2b0c83c" + ], + "index": "pypi", + "version": "==1.8.4" + }, + "sphinxcontrib-websupport": { + "hashes": [ + "sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd", + "sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9" + ], + "version": "==1.1.0" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + } +} diff --git a/docs/Makefile b/docs/Makefile index 9da4f30..db149d5 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -5,8 +5,8 @@ SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = photon-pump -SOURCEDIR = . -BUILDDIR = _build +SOURCEDIR = source +BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @@ -17,4 +17,4 @@ help: # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..543c6b1 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 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 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..4f5e9e3 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# 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('.')) + + +# -- Project information ----------------------------------------------------- + +project = "Punq" +copyright = "2019, Bob Gregory" +author = "Bob Gregory" + +# The short X.Y version +version = "" +# The full version, including alpha/beta/rc tags +release = "" + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ["sphinx.ext.autodoc", "sphinx.ext.coverage", "sphinx.ext.viewcode"] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The master toctree document. +master_doc = "index" + +# 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 + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- 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" + +# 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 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"] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = "Punqdoc" + + +# -- 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, "Punq.tex", "Punq Documentation", "Bob Gregory", "manual") +] + + +# -- 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, "punq", "Punq Documentation", [author], 1)] + + +# -- 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, + "Punq", + "Punq Documentation", + author, + "Punq", + "One line description of project.", + "Miscellaneous", + ) +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ["search.html"] + + +# -- Extension configuration ------------------------------------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..8ba4f90 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,20 @@ +.. Punq documentation master file, created by + sphinx-quickstart on Tue Feb 5 18:19:18 2019. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Punq's documentation! +================================ + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/punq/__init__.py b/punq/__init__.py index 7542ae6..6232899 100644 --- a/punq/__init__.py +++ b/punq/__init__.py @@ -1,27 +1,39 @@ -import itertools -from collections import defaultdict, namedtuple +import sys import typing -import logging +from collections import defaultdict, namedtuple +from pkg_resources import DistributionNotFound, get_distribution + +if sys.version_info >= (3, 7, 0): + from .py_37 import is_generic_list +else: + from .py_36 import is_generic_list -class MissingDependencyException (Exception): +try: + __version__ = get_distribution(__name__).version +except DistributionNotFound: + # package is not installed pass -class InvalidRegistrationException (Exception): +class MissingDependencyException(Exception): pass -Registration = namedtuple('Registration', ['service', 'builder', 'needs', 'args']) +class InvalidRegistrationException(Exception): + pass -class Registry: +Registration = namedtuple("Registration", ["service", "builder", "needs", "args"]) + +class Registry: def __init__(self): self.__registrations = defaultdict(list) def _get_needs_for_ctor(self, cls): sig = typing.get_type_hints(cls.__init__) + return sig def register_service_and_impl(self, service, impl, resolve_args): @@ -44,11 +56,9 @@ def register_service_and_impl(self, service, impl, resolve_args): >>> instance.send("Hello") >>> Sending message via smtp """ - self.__registrations[service].append(Registration( - service, - impl, - self._get_needs_for_ctor(impl), - resolve_args)) + self.__registrations[service].append( + Registration(service, impl, self._get_needs_for_ctor(impl), resolve_args) + ) def register_service_and_instance(self, service, instance): """Register a singleton instance to implement a service. @@ -68,11 +78,9 @@ def register_service_and_instance(self, service, instance): >>> container.register( ... DataAccessLayer, ... SqlAlchemyDataAccessLayer(create_engine(db_uri)))""" - self.__registrations[service].append(Registration( - service, - lambda: instance, - {}, - {})) + self.__registrations[service].append( + Registration(service, lambda: instance, {}, {}) + ) def register_concrete_service(self, service): """ Register a service as its own implementation. @@ -90,23 +98,25 @@ def register_concrete_service(self, service): if not type(service) is type: raise InvalidRegistrationException( - "The service %s can't be registered as its own implementation" % - (repr(service))) - self.__registrations[service].append(Registration( - service, - service, - self._get_needs_for_ctor(service), - {})) + "The service %s can't be registered as its own implementation" + % (repr(service)) + ) + self.__registrations[service].append( + Registration(service, service, self._get_needs_for_ctor(service), {}) + ) def build_context(self, key, existing=None): if existing is None: return ResolutionContext(key, list(self.__getitem__(key))) + if key not in existing.targets: existing.targets[key] = ResolutionTarget(key, list(self.__getitem__(key))) + return existing def register(self, service, _factory=None, **kwargs): resolve_args = kwargs or {} + if _factory is None: self.register_concrete_service(service) elif callable(_factory): @@ -123,17 +133,12 @@ def registrations(self): class ResolutionTarget: - def __init__(self, key, impls): self.service = key self.impls = impls def is_generic_list(self): - try: - if self.service.__origin__ == typing.List: - return self.service.__args__[0] - except AttributeError as e: - return None + return is_generic_list(self.service) @property def generic_parameter(self): @@ -145,7 +150,6 @@ def next_impl(self): class ResolutionContext: - def __init__(self, key, impls): self.targets = {key: ResolutionTarget(key, impls)} self.cache = {} @@ -168,7 +172,6 @@ def all_registrations(self, service): class Container: - def __init__(self): self.registrations = Registry() @@ -177,8 +180,10 @@ def register(self, service, _factory=None, **kwargs): def resolve_all(self, service, **kwargs): context = self.registrations.build_context(service) + return [ - self._build_impl(x, kwargs, context) for x in context.all_registrations(service) + self._build_impl(x, kwargs, context) + for x in context.all_registrations(service) ] def _build_impl(self, registration, resolution_args, context): @@ -188,35 +193,40 @@ def _build_impl(self, registration, resolution_args, context): args = { k: self._resolve_impl(v, resolution_args, context) for k, v in registration.needs.items() - if k != 'return' and k not in registration.args - + if k != "return" and k not in registration.args } args.update(registration.args) args.update(resolution_args or {}) result = registration.builder(**args) context[registration.service] = result + return result def _resolve_impl(self, service_key, kwargs, context): context = self.registrations.build_context(service_key, context) + if context.has_cached(service_key): return context[service_key] target = context.target(service_key) + if target.is_generic_list(): return self.resolve_all(target.generic_parameter) registration = target.next_impl() + if registration is None: raise MissingDependencyException( - 'Failed to resolve implementation for '+str(service_key)) + "Failed to resolve implementation for " + str(service_key) + ) if service_key in registration.needs.values(): self._resolve_impl(service_key, kwargs, context) - return self._build_impl(registration, kwargs, context) + return self._build_impl(registration, kwargs, context) def resolve(self, service_key, **kwargs): context = self.registrations.build_context(service_key) + return self._resolve_impl(service_key, kwargs, context) diff --git a/punq/py_36.py b/punq/py_36.py new file mode 100644 index 0000000..e3ac503 --- /dev/null +++ b/punq/py_36.py @@ -0,0 +1,8 @@ +from typing import List + + +def is_generic_list(service): + try: + return service.__origin__ == List + except AttributeError: + return False diff --git a/punq/py_37.py b/punq/py_37.py new file mode 100644 index 0000000..5b026cc --- /dev/null +++ b/punq/py_37.py @@ -0,0 +1,5 @@ +def is_generic_list(service): + try: + return service.__origin__ == list + except AttributeError: + return False diff --git a/setup.py b/setup.py index 8f779b8..3811c57 100644 --- a/setup.py +++ b/setup.py @@ -1,48 +1,46 @@ import io -import sys from setuptools import setup def read(*filenames, **kwargs): - encoding = kwargs.get('encoding', 'utf-8') - sep = kwargs.get('sep', '\n') + encoding = kwargs.get("encoding", "utf-8") + sep = kwargs.get("sep", "\n") buf = [] + for filename in filenames: with io.open(filename, encoding=encoding) as f: buf.append(f.read()) + return sep.join(buf) -long_description = read('README.rst', 'CHANGES.md') +long_description = read("README.rst", "CHANGES.md") setup( - name='punq', - version='0.1.0', - url='http://github.com/bobthemighty/punq', - license='MIT', - author='Bob Gregory', - tests_require=[ - 'colorama==0.3.9', - 'Contexts==0.11.2', - 'expects==0.8.0' - ], - author_email='bob@codefiend.co.uk', - description='Unintrusive dependency injection for Python 3.6 +', + name="punq", + use_scm_version=True, + setup_requires=["setuptools_scm"], + url="http://github.com/bobthemighty/punq", + license="MIT", + author="Bob Gregory", + tests_require=["colorama==0.3.9", "Contexts==0.11.2", "expects==0.8.0"], + author_email="bob@codefiend.co.uk", + description="Unintrusive dependency injection for Python 3.6 +", long_description=long_description, - packages=['punq'], - package_data={'punq': ['CHANGES.md']}, + packages=["punq"], + package_data={"punq": ["CHANGES.md"]}, include_package_data=True, - platforms='any', + platforms="any", classifiers=[ - 'Programming Language :: Python', - 'Development Status :: 3 - Alpha', - 'Natural Language :: English', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Software Development :: Libraries :: Application Frameworks' - ] + "Programming Language :: Python", + "Development Status :: 3 - Alpha", + "Natural Language :: English", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Libraries :: Application Frameworks", + ], ) diff --git a/tests/test_instance_creation.py b/tests/test_instance_creation.py index 4289d9c..cf579b3 100644 --- a/tests/test_instance_creation.py +++ b/tests/test_instance_creation.py @@ -2,24 +2,23 @@ from punq import Container, MissingDependencyException, InvalidRegistrationException from typing import Callable, List, NewType -class MessageWriter: +class MessageWriter: def write(self, msg: str) -> None: pass -class MessageSpeaker: +class MessageSpeaker: def speak(self): pass -class StdoutMessageWriter(MessageWriter): +class StdoutMessageWriter(MessageWriter): def write(self, msg: str) -> None: print(msg) class TmpFileMessageWriter(MessageWriter): - def __init__(self, path): self.path = path @@ -28,20 +27,18 @@ def write(self, msg: str) -> None: f.write(msg) -ConnectionStringFactory = NewType( - 'ConnectionStringFactory', - Callable[[], str]) +ConnectionStringFactory = NewType("ConnectionStringFactory", Callable[[], str]) -class FancyDbMessageWriter(MessageWriter): +class FancyDbMessageWriter(MessageWriter): def __init__(self, cstr: ConnectionStringFactory) -> None: self.connection_string = cstr() def write(self, msg): pass -class HelloWorldSpeaker(MessageSpeaker): +class HelloWorldSpeaker(MessageSpeaker): def __init__(self, writer: MessageWriter) -> None: self.writer = writer @@ -50,7 +47,6 @@ def speak(self): class When_creating_instances_with_no_dependencies: - def given_a_container(self): self.container = Container() self.container.register(MessageWriter, StdoutMessageWriter) @@ -63,7 +59,6 @@ def it_should_be_an_instance_of_the_registered_implementor(self): class When_creating_instances_with_dependencies: - def given_a_container(self): self.container = Container() self.container.register(MessageWriter, StdoutMessageWriter) @@ -80,7 +75,6 @@ def it_should_have_injected_the_dependency(self): class When_a_dependency_is_missing: - def given_a_container(self): self.container = Container() self.container.register(MessageSpeaker, HelloWorldSpeaker) @@ -96,7 +90,6 @@ def it_should_raise_an_exception(self): class When_registering_a_service_with_no_implementor: - def given_a_container(self): self.container = Container() @@ -106,11 +99,11 @@ def because_we_register_a_service_as_itself(self): def it_should_register_as_its_own_implementor(self): expect(self.container.resolve(StdoutMessageWriter)).to( - be_a(StdoutMessageWriter)) + be_a(StdoutMessageWriter) + ) class When_registering_a_service_with_a_custom_factory: - def given_a_container(self): self.container = Container() @@ -126,10 +119,9 @@ def it_should_have_resolved_the_dependency(self): class When_registering_a_service_with_a_singleton_instance: - def given_a_container_with_a_singleton_registration(self): self.container = Container() - self.writer = TmpFileMessageWriter('/tmp/my-file') + self.writer = TmpFileMessageWriter("/tmp/my-file") self.container.register(MessageWriter, self.writer) def because_we_resolve_the_instance(self): @@ -140,7 +132,6 @@ def it_should_return_the_singleton_instance(self): class When_registering_a_concrete_service_as_a_singleton: - def given_a_container(self): self.container = Container() self.writer = StdoutMessageWriter() @@ -173,13 +164,12 @@ def it_should_have_raised_InvalidRegistration(self): class When_registering_the_same_service_multiple_times: - def given_a_container(self): self.container = Container() def because_we_register_two_writers(self): self.container.register(MessageWriter, StdoutMessageWriter) - self.container.register(MessageWriter, TmpFileMessageWriter('my-file')) + self.container.register(MessageWriter, TmpFileMessageWriter("my-file")) def it_should_resolve_the_latest_registration(self): expect(self.container.resolve(MessageWriter)).to(be_a(TmpFileMessageWriter)) @@ -195,10 +185,11 @@ def it_should_return_the_implementations_in_registration_order(self): class When_registering_a_service_and_providing_an_argument: - def given_a_container(self): self.container = Container() - self.container.register(MessageWriter, FancyDbMessageWriter, cstr=lambda: "Hello world") + self.container.register( + MessageWriter, FancyDbMessageWriter, cstr=lambda: "Hello world" + ) def because_we_resolve_an_instance(self): self.instance = self.container.resolve(MessageWriter) @@ -210,32 +201,27 @@ def it_should_have_passed_the_static_argument(self): expect(self.instance.connection_string).to(equal("Hello world")) - class When_we_provide_an_argument_at_resolution_time: - def given_a_container(self): self.container = Container() self.container.register(MessageWriter, TmpFileMessageWriter) def because_we_resolve_with_an_argument(self): - self.instance = self.container.resolve(MessageWriter, path='foo') + self.instance = self.container.resolve(MessageWriter, path="foo") def it_should_have_instantiated_the_instance_correctly(self): - expect(self.instance.path).to(equal('foo')) + expect(self.instance.path).to(equal("foo")) class When_we_need_to_resolve_a_list_of_dependencies: - class BroadcastSpeaker: - def __init__(self, writers: List[MessageWriter]) -> None: self.writers = writers - def given_a_container(self): self.container = Container() self.container.register(MessageWriter, StdoutMessageWriter) - self.container.register(MessageWriter, TmpFileMessageWriter, path='my-file') + self.container.register(MessageWriter, TmpFileMessageWriter, path="my-file") self.container.register(MessageSpeaker, self.BroadcastSpeaker) def because_we_depend_on_a_list_of_registered_dependencies(self): @@ -249,47 +235,50 @@ class Filter: pass -class Is_A (Filter): - def __init__(self, next:Filter, spy): +class Is_A(Filter): + def __init__(self, next: Filter, spy): self.spy = spy self.next = next def match(self, input): - self.spy.append('is_a') - return input == 'A' or self.next.match(input) + self.spy.append("is_a") + + return input == "A" or self.next.match(input) -class Is_B (Filter): - def __init__(self, next:Filter, spy): + +class Is_B(Filter): + def __init__(self, next: Filter, spy): self.spy = spy self.next = next def match(self, input): - self.spy.append('is_b') - return input == 'B' or self.next.match(input) + self.spy.append("is_b") + + return input == "B" or self.next.match(input) -class Is_C (Filter): - def __init__(self, next:Filter, spy): + +class Is_C(Filter): + def __init__(self, next: Filter, spy): self.spy = spy self.next = next def match(self, input): - self.spy.append('is_c') - return input == 'C' or self.next.match(input) + self.spy.append("is_c") + + return input == "C" or self.next.match(input) + -class NullFilter (Filter): +class NullFilter(Filter): def __init__(self, spy): self.spy = spy def match(self, input): - self.spy.append('null') - return False - + self.spy.append("null") + return False class When_we_need_to_resolve_a_chain_of_collaborators: - - def given_a_container(self): self.spy = [] self.container = Container() @@ -300,8 +289,7 @@ def given_a_container(self): def because_we_resolve_an_instance_of_the_chain(self): self.filter = self.container.resolve(Filter) - self.filter.match('D') + self.filter.match("D") def it_should_call_each_element_in_turn(self): - expect(self.spy).to(equal(['is_a', 'is_b', 'is_c', 'null'])) - + expect(self.spy).to(equal(["is_a", "is_b", "is_c", "null"]))