diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89123cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Project-specific generated files +docs/build/ + +bench/results/ +bench/env/ +bench/trio/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*~ +\#* +.#* + +# C extensions +*.so + +# Distribution / packaging +.Python +/build/ +/develop-eggs/ +/dist/ +/eggs/ +/lib/ +/lib64/ +/parts/ +/sdist/ +/var/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +# Sphinx documentation +doc/_build/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5a85ed1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: python +python: + - 3.5 + - 3.6 +sudo: false +dist: trusty + +script: + - ci/travis.sh diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..c4e2a59 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,50 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of +fostering an open and welcoming community, we pledge to respect all people who +contribute through reporting issues, posting feature requests, updating +documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free +experience for everyone, regardless of level of experience, gender, gender +identity and expression, sexual orientation, disability, personal appearance, +body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic + addresses, without explicit permission +* Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to +fairly and consistently applying these principles to every aspect of managing +this project. Project maintainers who do not follow or enforce the Code of +Conduct may be permanently removed from the project team. + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting a project maintainer at njs@pobox.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. Maintainers are +obligated to maintain confidentiality with regard to the reporter of an +incident. + + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.3.0, available at +[http://contributor-covenant.org/version/1/3/0/][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/3/0/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6ff5c25 --- /dev/null +++ b/LICENSE @@ -0,0 +1,3 @@ +This software is made available under the terms of *either* of the +licenses found in LICENSE.APACHE2 or LICENSE.MIT. Contributions to +trio are made under the terms of *both* these licenses. diff --git a/LICENSE.APACHE2 b/LICENSE.APACHE2 new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.APACHE2 @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE.MIT b/LICENSE.MIT new file mode 100644 index 0000000..b8bb971 --- /dev/null +++ b/LICENSE.MIT @@ -0,0 +1,20 @@ +The MIT License (MIT) + +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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2ffe070 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include LICENSE LICENSE.MIT LICENSE.APACHE2 +include README.rst +include CODE_OF_CONDUCT.md +recursive-include docs * +prune docs/build diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..ce19680 --- /dev/null +++ b/README.rst @@ -0,0 +1,39 @@ +.. note that this README gets 'include'ed into the main documentation + +sphinxcontrib-trio is a sphinx extension to help with documenting +Python code that uses async/await, or abstract methods, or context +managers, or generators, or ... you get the idea. It makes sphinx's +regular directives for documenting Python functions and methods +smarter and more powerful. The name is because it was originally +written for the `Trio `__ project, and +I'm not very creative. But don't be put off – there's nothing Trio- or +async-specific about this extension; any Python project can +benefit. (Though projects using async/await probably benefit the most, +since sphinx's built-in tools are especially inadequate in this case.) + + +Vital statistics +---------------- + +**Requirements:** This extension currently assumes you're using Python +3.5+ to build your docs. This could be relaxed if anyone wants to send +a patch. + +**Documentation:** https://sphinxcontrib-trio.readthedocs.io + +**Bug tracker and source code:** +https://github.com/python-trio/sphinxcontrib-trio + +**License:** MIT or Apache 2, your choice. + +**Usage:** ``pip install -U sphinxcontrib-trio`` in the same +environment where you installed sphinx, and then add +``"sphinxcontrib_trio"`` to the list of ``extensions`` in your +project's ``conf.py``. (Note that it's ``sphinxcontrib_trio`` with an +underscore, NOT a dot. This is because I don't understand namespace +packages, and I fear things that I don't understand.) + +**Code of conduct:** Contributors are requested to follow our `code of +conduct +`__ +in all project spaces. diff --git a/ci/rtd-requirements.txt b/ci/rtd-requirements.txt new file mode 100644 index 0000000..90344f5 --- /dev/null +++ b/ci/rtd-requirements.txt @@ -0,0 +1,2 @@ +# RTD is currently installing 1.5.3, which has a bug in :lineno-match: +git+git://github.com/sphinx-doc/sphinx.git@dc61f68e141ab3d6bdaf7d94aa4b942d4821dfdb diff --git a/ci/travis.sh b/ci/travis.sh new file mode 100755 index 0000000..73190d9 --- /dev/null +++ b/ci/travis.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -exu -o pipefail + +pip install -U pip setuptools wheel + +python setup.py sdist --formats=zip +pip install dist/*.zip + +cd docs +# -n (nit-picky): warn on missing references +# -W: turn warnings into errors +sphinx-build -nW -b html source build diff --git a/docs/source/_static/hack.css b/docs/source/_static/hack.css new file mode 100644 index 0000000..b819e3d --- /dev/null +++ b/docs/source/_static/hack.css @@ -0,0 +1,36 @@ +/* docutils has a very stupid heuristic where it forces browsers to render the + columns in HTML tables with the same relative widths as the monospaced ReST + source text for those columns. Our tables have lots of markup in them that + take up source text but don't even appear in the HTML (and never mind that + the whole thing makes no sense because browsers know the *actual* width of + the text), which causes them to be ugly and have unnecessary linebreaks in + some columns and random whitespace in others. + + According to the docutils documentation, this can be overridden by using a + table:: directive with :widths: auto, or setting the docutils configuration + table_style to "colwidths-auto". However, after much pain and hassle, I've + convinced myself that this doesn't actually work -- the code in + _html_base.py in docutils that tries to decide whether to emit the + tag based on these settings isn't even called, and the code in + html4css1/__init__.py that actually emits the tags doesn't check + the configuration. (Also, sphinx makes it absurdly difficult to modify the + docutils settings -- I ended up monkeypatching + sphinx.environment.default_settings. But it doesn't matter because the + settings don't work anyway.) + + This attempts to tell the browser to ignore docutils's helpful table width + settings. I'm not sure it works on all browsers, because docutils actually + generates HTML like: + + + + + + + but in HTML5 the HTML width= attribute on tags is deprecated in favor + CSS width: attribute, so hopefully most browsers map them to the same + internal setting. It works on Firefox 52, anyway. +*/ +table.docutils col { + width: auto !important; +} diff --git a/docs/source/_templates/.gitkeep b/docs/source/_templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/source/conf.py b/docs/source/conf.py index f886bd5..c0917a8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -37,7 +37,11 @@ 'sphinxcontrib_trio', ] +def setup(app): + app.add_stylesheet("hack.css") + intersphinx_mapping = { + #"sphinx": ("http://www.sphinx-doc.org/en/stable/", None), #"python": ('https://docs.python.org/3', None), } diff --git a/docs/source/index.rst b/docs/source/index.rst index ee10358..d45c740 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,78 +1,30 @@ -sphinxcontrib-trio -================== +sphinxcontrib-trio documentation +================================ -sphinxcontrib-trio is a sphinx extension that makes sphinx's -directives for documenting Python functions and methods smarter and -more powerful. The name is because it was originally written for the -`Trio `__ project, and I'm not very -creative. Don't be put off – while it's especially useful for projects -using async/await, there's nothing Trio- or async-specific about it; -any project can benefit. +.. include:: ../../README.rst -**Requirements:** This extension currently assumes you're using Python -3.5+ to build your docs. This could be relaxed if anyone wants to send -a patch. -**Documentation:** https://sphinxcontrib-trio.readthedocs.io +The big idea +------------ -**Bug tracker and source code:** -https://github.com/python-trio/sphinxcontrib-trio - -**License:** XX - - -Idea ----- - -Sphinx provides a number of convenient directives for `documenting -Python code +Sphinx provides some convenient directives for `documenting Python +code `__: -you can use the ``.. method::`` directive to document an ordinary -method, the ``.. classmethod::`` directive to document a classmethod, -the ``.. decoratormethod::`` directive to document a decorator method, -and so on. But what if you have a classmethod that's also a decorator? -And what if you want to document a project that uses some of Python's -many interesting function types that Sphinx *doesn't* support, like -async functions, abstract methods, or generators? +you can use the ``method::`` directive to document an ordinary method, +the ``classmethod::`` directive to document a classmethod, the +``decoratormethod::`` directive to document a decorator method, and so +on. But what if you have a classmethod that's also a decorator? And +what if you want to document a project that uses some of Python's many +interesting function types that Sphinx *doesn't* support, like async +functions, abstract methods, generators, ...? It would be possible to keep adding directive after directive for -every possible type – ``.. asyncmethod::``, ``.. abstractmethod::``, -``.. classmethoddecorator::``, ``.. abstractasyncstaticmethod::``, but -this quickly becomes silly. This package takes a different approach: - -but we take a different approach, based on the -observation that "classmethod" ,"async", "abstractmethod" are -*attributes* of a function, not *types* of function: in particular, -they can be mixed and matched! - - -When I was documenting the `trio `__ -package, - -I realized I had a problem. I was using `sphinx -`__, of course. And sphinx provides some -`convenient directives for documenting Python code -`__: -you can use the ``.. method::`` directive to document an ordinary -method, the ``.. classmethod::`` directive to document a classmethod, -the ``.. decoratormethod::`` directive to document a decorator method, -and so on. But I realized that I had a lots of async methods that I -wanted to document. And abstract methods. And context managers. And -sphinx doesn't have directives for any of these. So maybe I needed to -add them? That's annoying but not *so* bad, so I studiously started -doing that, one by one... - -\...until I realized that I also had an abstract async classmethod, -started to type ``.. abstractasyncclassmethod::``, and realized that -something had gone very wrong. It doesn't make sense to have a totally -new and unique directive for every possible combination of attributes -that a function or method might have! This just doesn't scale. - -That's where ``sphinxcontrib-trio`` comes in: instead of having a -different top-level directive for every *combination* of attributes, -it makes it so the basic ``.. function::`` and ``.. method::`` -directives now accept options describing each *individual* attribute -that your function/method has, so you can mix and match like: +every possible type: ``asyncmethod::``, ``abstractmethod::``, +``classmethoddecorator::``, ``abstractasyncstaticmethod::`` – you get +the idea. But this quickly becomes silly. sphinxcontrib-trio takes a +different approach: it enhances the basic ``function::`` and +``method::`` directives to accept options describing the attributes of +each function/method, so you can mix and match like: .. code-block:: rst @@ -81,26 +33,30 @@ that your function/method has, so you can mix and match like: :async: :classmethod: -This renders like: + This method is perhaps more complicated than it needs to be. + +and you'll get a rendered output like: .. method:: overachiever(arg1, ...) :abstractmethod: :async: :classmethod: -And while I was at it, I also replaced the ``sphinx.ext.autodoc`` -directives ``.. autofunction::`` and ``.. automethod::`` with new -versions that know how to automatically detect many of these -attributes, so you could just as easily have written the above as: + This method is perhaps more complicated than it needs to be. + +And while I was at it, I also enhanced the ``sphinx.ext.autodoc`` +directives ``autofunction::`` and ``automethod::`` with new versions +that know how to automatically detect many of these attributes, so you +could just as easily have written the above as: .. code-block:: rst - .. automethod:: my_method + .. automethod:: overachiever and it would automatically figure out that this was an abstract async classmethod by looking at your code. -And finally, I made the legacy ``.. classmethod::`` directive into an +And finally, I made the legacy ``classmethod::`` directive into an alias for: .. code-block:: rst @@ -110,7 +66,7 @@ alias for: and similarly ``staticmethod``, ``decorator``, and ``decoratormethod``, so dropping this extension into an existing -sphinx project should be 100% backwards compatible while giving you +sphinx project should be 100% backwards-compatible while giving you new superpowers. Basically, this is how sphinx ought to work in the first @@ -121,36 +77,40 @@ is pretty handy. Details on supported options ---------------------------- -The enhanced ``.. function::`` directive supports the following -options, some of which can be automatically detected by -``.. autofunction::``: - -==================== ========================== ====================== -Spelling Renders like Detected by autodoc? -==================== ========================== ====================== -``:async:`` await fn() yes! -``:decorator:`` @fn no -``:with:`` with fn() no -``:with: foo`` with fn() as foo no -``:async-with:`` async with fn() no -``:async-with: foo`` async with fn() as foo no -``:for:`` for ... in fn() generators only -``:for: foo`` with foo in fn() generators only -``:async-for:`` async for ... in fn() async generators only -``:async-for: foo`` async for foo in fn() async generators only -==================== ========================== ====================== - -The enhanced ``.. method::`` and ``.. automethod::`` directives -support all of the same options as the ``.. function::`` and -``.. autofunction::`` directives, and also add the following: - -==================== ========================== ==================== -Spelling Renders like Detected by autodoc? -==================== ========================== ==================== -``:abstractmethod:`` abstractmethod fn() yes! -``:staticmethod:`` staticmethod fn() yes! -``:classmethod:`` classmethod fn() yes! -==================== ========================== ==================== +The following options are supported by the enhanced ``function::`` and +``method::`` directives, and some of them can be automatically +detected if you use ``autofunction::`` / ``automethod::``: + +==================== =============================== ========================== +Option Renders like Autodetectable? +==================== =============================== ========================== +``:async:`` *await* **fn**\() yes! +``:decorator:`` @\ **fn** no +``:with:`` *with* **fn**\() no +``:with: foo`` *with* **fn**\() *as foo* no +``:async-with:`` *async with* **fn**\() no +``:async-with: foo`` *async with* **fn**\() *as foo* no +``:for:`` *for ... in* **fn**\() yes! (on generators) +``:for: foo`` *for foo in* **fn**\() yes! (on generators) +``:async-for:`` *async for ... in* **fn**\() yes! (on async generators) +``:async-for: foo`` *async for foo in* **fn**\() yes! (on async generators) +==================== =============================== ========================== + +The ``:async-for:`` autodetection code supports both `native async +generators `__ (in Python +3.6+) and those created by the `async_generator +`__ library (in Python +3.5+). + +There are also a few options that are specific to ``method::``: + +==================== ========================== ===================== +Option Renders like Autodetectable? +==================== ========================== ===================== +``:abstractmethod:`` *abstractmethod* **fn**\() yes! +``:staticmethod:`` *staticmethod* **fn**\() yes! +``:classmethod:`` *classmethod* **fn**\() yes! +==================== ========================== ===================== Examples @@ -163,94 +123,118 @@ A regular async function: .. function:: example_async_fn(...) :async: + This is an example. + Renders as: .. function:: example_async_fn(...) :async: + This is an example. + A context manager with a hint as to what's returned: .. code-block:: rst - .. function:: open(fname) + .. function:: open(file_name) :with: file_handle + It's good practice to use :func:`open` as a context manager. + Renders as: -.. function:: open(fname) +.. function:: open(file_name) :with: file_handle - -.. what happens if we autodetect a generator then someone adds ':for: - x', so we have :for: and then :for x:? + It's good practice to use :func:`open` as a context manager. The auto versions of the directives also accept explicit options, which are appended to the automatically detected options. So if -``some_method`` is defined as a ``classmethod`` in the source, and you -want to document that it should be used as a context manager, you can +``some_method`` is defined as a ``abstractmethod`` in the source, and +you want to document that it should be used as a decorator, you can write: .. code-block:: rst .. automethod:: some_method - :with: + :decorator: -then it will render like: +and it will render like: -.. method:: some_method(arg1, ...) - :classmethod: - :with: +.. method:: some_method + :abstractmethod: + :decorator: - This method is very interesting because ... [docstring pulled from source] + Here's some text automatically extracted from the method's docstring. Bugs and limitations -------------------- -Currently there are no tests, because I don't know how to test a -sphinx extension. If you do, please let me know. +* Currently there are no tests, because I don't know how to test a + sphinx extension. If you do, please let me know. -Python supports defining abstract properties like:: +* Python supports defining abstract properties like:: - @abstractmethod - @property - def some_property(...): - ... + @abstractmethod + @property + def some_property(...): + ... -But this extension currently doesn't help you document them. The -difficulty is that for Sphinx, properties are attributes, not methods, -and we don't currently hook the code for handling ``.. attribute::`` -and ``.. autoattribute::``. Maybe we should? + But currently this extension doesn't help you document them. The + difficulty is that for Sphinx, properties are "attributes", not + "methods", and we don't currently hook the code for handling + ``attribute::`` and ``autoattribute::``. Maybe we should? -When multiple options are combined, then we try to render them in a -sensible way, but this does assume that you're giving us a sensible -combination to start with. If you give sphinxcontrib-trio nonsense, -then it will happily render nonsense. For example, this ReST: +* When multiple options are combined, then we try to render them in a + sensible way, but this does assume that you're giving us a sensible + combination to start with. If you give sphinxcontrib-trio nonsense, + then it will happily render nonsense. For example, this ReST: -.. code-block:: rst + .. code-block:: rst - .. function:: all_things_to_all_people(a, b) - :with: x - :async-with: y - :for: z - :decorator: + .. function:: all_things_to_all_people(a, b) + :with: x + :async-with: y + :for: z + :decorator: -renders as: + Something has gone terribly wrong. -.. function:: all_things_to_all_people(a, b) - :with: x - :async-with: y - :for: z - :decorator: + renders as: + + .. function:: all_things_to_all_people(a, b) + :with: x + :async-with: y + :for: z + :decorator: + + Something has gone terribly wrong. + +* There's currently no particular support for asyncio's old-style + "generator-based coroutines", though they might work if you remember + to use `asyncio.coroutine + `__. Acknowledgements ---------------- Inspiration and hints on sphinx hackery were drawn from: + * `sphinxcontrib-asyncio `__ * `Curio's local customization `__ * `CPython's local customization `__ + +sphinxcontrib-asyncio was especially helpful. Compared to +sphinxcontrib-asyncio, this package takes the idea of directive +options to its logical conclusion, steals Dave Beazley's idea of +documenting special methods like coroutines by showing how they're +used ("await f()" instead of "coroutine f()"), and avoids the +`forbidden word +`__ +`coroutine +`__. diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6144b9e --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +from pathlib import Path +from setuptools import setup + +from sphinxcontrib_trio import __version__ + +setup( + name="sphinxcontrib-trio", + version=__version__, + description= + "Make Sphinx better at documenting Python functions and methods", + # Just in case the cwd is not the root of the source tree, or python is + # not set to use utf-8 by default: + long_description=Path(__file__).with_name("README.rst").read_text('utf-8'), + author="Nathaniel J. Smith", + author_email="njs@pobox.com", + license="MIT -or- Apache License 2.0", + py_modules="sphinxcontrib_trio", + url="https://github.com/python-trio/sphinxcontrib-trio", + classifiers=[ + 'Development Status :: 5 - Production/Stable', + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Framework :: Sphinx :: Extension", + "Topic :: Documentation :: Sphinx", + "Topic :: Software Development :: Documentation", + ]) diff --git a/sphinxcontrib_trio.py b/sphinxcontrib_trio.py new file mode 100644 index 0000000..229050c --- /dev/null +++ b/sphinxcontrib_trio.py @@ -0,0 +1,314 @@ +__version__ = "0.9.0" + +"""A sphinx extension to help documenting Python code that uses async/await +(or context managers, or abstract methods, or generators, or ...). + +This extension takes a somewhat non-traditional approach, though, based on +the observation that function properties like "classmethod", "async", +"abstractmethod" can be mixed and matched, so the the classic sphinx +approach of defining different directives for all of these quickly becomes +cumbersome. Instead, we override the ordinary function & method directives +to add options corresponding to these different properties, and override the +autofunction and automethod directives to sniff for these +properties. Examples: + +A function that returns a context manager: + + .. function:: foo(x, y) + :with: bar + +renders in the docs like: + + with foo(x, y) as bar + +The 'bar' part is optional. Use :async-with: for an async context +manager. These are also accepted on method, autofunction, and automethod. + +An abstract async classmethod: + + .. method:: foo + :abstractmethod: + :classmethod: + :async: + +renders like: + + abstractmethod classmethod await foo() + +Or since all of these attributes are introspectable, we can get the same +result with: + + .. automethod:: foo + +An abstract static decorator: + + .. method:: foo + :abstractmethod: + :staticmethod: + :decorator: + +The :decorator: attribute isn't introspectable, but the others +are, so this also works: + + .. automethod:: foo + :decorator: + +and renders like + + abstractmethod staticmethod @foo + +""" + +import inspect +import async_generator + +from docutils.parsers.rst import directives +from sphinx import addnodes +from sphinx.domains.python import PyModulelevel, PyClassmember +from sphinx.ext.autodoc import ( + FunctionDocumenter, MethodDocumenter, ClassLevelDocumenter, +) + +extended_function_option_spec = { + "async": directives.flag, + "decorator": directives.flag, + "with": directives.unchanged, + "async-with": directives.unchanged, + "for": directives.unchanged, + "async-for": directives.unchanged, +} + +extended_method_option_spec = { + **extended_function_option_spec, + "abstractmethod": directives.flag, + "staticmethod": directives.flag, + "classmethod": directives.flag, + #"property": directives.flag, +} + + +################################################################ +# Extending the basic function and method directives +################################################################ + +class ExtendedCallableMixin: + def needs_arglist(self): + # if "property" in self.options: + # return False + if "decorator" in self.options or self.objtype == "decorator": + return False + return True + + # This does *not* override the superclass get_signature_prefix(), because + # that gets called by the superclass handle_signature(), which then + # may-or-may-not insert it into the signode (depending on whether or not + # it returns an empty string). We want to insert the decorator @ after the + # prefix but before the regular name. If we let the superclass + # handle_signature() insert the prefix or maybe not, then we can't tell + # where the @ goes. + def _get_signature_prefix(self): + ret = "" + if "abstractmethod" in self.options: + ret += "abstractmethod " + # objtype checks are for backwards compatibility, to support + # + # .. staticmethod:: + # + # in addition to + # + # .. method:: + # :staticmethod: + # + # it would be nice if there were a central place we could normalize + # the directive name into the options dict instead of having to check + # both here at time-of-use, but I don't understand sphinx well enough + # to do that. + # + # Note that this is the code that determines the ordering of the + # different prefixes. + if "staticmethod" in self.options or self.objtype == "staticmethod": + ret += "staticmethod " + if "classmethod" in self.options or self.objtype == "classmethod": + ret += "classmethod " + # if "property" in self.options: + # ret += "property " + if "with" in self.options: + ret += "with " + if "async-with" in self.options: + ret += "async with " + for for_type, render in [("for", "for"), ("async-for", "async for")]: + if for_type in self.options: + name = self.options.get(for_type, "") + if not name.strip(): + name = "..." + ret += "{} {} in ".format(render, name) + if "async" in self.options: + ret += "await " + return ret + + def handle_signature(self, sig, signode): + ret = super().handle_signature(sig, signode) + + # Add the "@" prefix + if "decorator" in self.options or self.objtype == "decorator": + signode.insert(0, addnodes.desc_addname("@", "@")) + + # Now that the "@" has been taken care of, we can add in the regular + # prefix. + prefix = self._get_signature_prefix() + if prefix: + signode.insert(0, addnodes.desc_annotation(prefix, prefix)) + + # And here's the suffix: + for optname in ["with", "async-with"]: + if self.options.get(optname, "").strip(): + # for some reason a regular space here gets stripped, so we + # use U+00A0 NO-BREAK SPACE + s = "\u00A0as {}".format(self.options[optname]) + signode += addnodes.desc_annotation(s, s) + + return ret + +class ExtendedPyFunction(ExtendedCallableMixin, PyModulelevel): + option_spec = { + **PyModulelevel.option_spec, + **extended_function_option_spec, + } + +class ExtendedPyMethod(ExtendedCallableMixin, PyClassmember): + option_spec = { + **PyClassmember.option_spec, + **extended_method_option_spec, + } + + +################################################################ +# Autodoc +################################################################ + +def sniff_options(obj): + options = set() + async_gen = False + # We walk the __wrapped__ chain to collect properties. + # + # If something sniffs as *both* an async generator *and* a coroutine, then + # it's probably an async_generator-style async_generator (since they wrap + # a coroutine, but are not a coroutine). + while True: + if getattr(obj, "__isabstractmethod__", False): + options.add("abstractmethod") + if isinstance(obj, classmethod): + options.add("classmethod") + if isinstance(obj, staticmethod): + options.add("staticmethod") + # if isinstance(obj, property): + # options.add("property") + if inspect.iscoroutinefunction(obj): + options.add("async") + # in versions of Python, isgeneratorfunction returns true for + # coroutines, so we use elif + elif inspect.isgeneratorfunction(obj): + options.add("for") + if async_generator.isasyncgenfunction(obj): + options.add("async-for") + async_gen = True + if hasattr(obj, "__wrapped__"): + obj = obj.__wrapped__ + else: + break + if async_gen: + options.discard("async") + return options + +def update_with_sniffed_options(obj, option_dict): + sniffed = sniff_options(obj) + for attr in sniffed: + # Suppose someone has a generator, and they document it as: + # + # .. autofunction:: my_generator + # :for: loop_var + # + # We don't want to blow away the existing attr["for"] = "loop_var" + # with our autodetected attr["for"] = None. So we use setdefault. + option_dict.setdefault(attr, None) + +def passthrough_option_lines(self, option_spec): + sourcename = self.get_sourcename() + for option in option_spec: + if option in self.options: + if self.options.get(option) is not None: + line = " :{}: {}".format(option, self.options[option]) + else: + line = " :{}:".format(option) + self.add_line(line, sourcename) + +class ExtendedFunctionDocumenter(FunctionDocumenter): + priority = FunctionDocumenter.priority + 1 + # You can explicitly set the options in case autodetection fails + option_spec = { + **FunctionDocumenter.option_spec, + **extended_function_option_spec, + } + + def add_directive_header(self, sig): + super().add_directive_header(sig) + passthrough_option_lines(self, extended_function_option_spec) + + def import_object(self): + ret = super().import_object() + update_with_sniffed_options(self.object, self.options) + return ret + +class ExtendedMethodDocumenter(MethodDocumenter): + priority = MethodDocumenter.priority + 1 + # You can explicitly set the options in case autodetection fails + option_spec = { + **MethodDocumenter.option_spec, + **extended_method_option_spec, + } + + def add_directive_header(self, sig): + super().add_directive_header(sig) + passthrough_option_lines(self, extended_method_option_spec) + + def import_object(self): + # MethodDocumenter overrides import_object to do some sniffing in + # addition to just importing. But we do our own sniffing and just want + # the import, so we un-override it. + ret = ClassLevelDocumenter.import_object(self) + # If you have a classmethod or staticmethod, then + # + # Class.__dict__["name"] + # + # returns the classmethod/staticmethod object, but + # + # getattr(Class, "name") + # + # returns a regular function. We want to detect + # classmethod/staticmethod, so we need to go through __dict__. + obj = self.parent.__dict__.get(self.object_name) + update_with_sniffed_options(obj, self.options) + # Replicate the special ordering hacks in + # MethodDocumenter.import_object + if "classmethod" in self.options or "staticmethod" in self.options: + self.member_order -= 1 + return ret + +################################################################ +# Register everything +################################################################ + +def setup(app): + app.add_directive_to_domain('py', 'function', ExtendedPyFunction) + app.add_directive_to_domain('py', 'method', ExtendedPyMethod) + app.add_directive_to_domain('py', 'classmethod', ExtendedPyMethod) + app.add_directive_to_domain('py', 'staticmethod', ExtendedPyMethod) + app.add_directive_to_domain('py', 'decorator', ExtendedPyFunction) + app.add_directive_to_domain('py', 'decoratormethod', ExtendedPyMethod) + + # We're overriding these on purpose, so disable the warning about it + del directives._directives["autofunction"] + del directives._directives["automethod"] + app.add_autodocumenter(ExtendedFunctionDocumenter) + app.add_autodocumenter(ExtendedMethodDocumenter) + return {'version': __version__, 'parallel_read_safe': True}