##// END OF EJS Templates
Fix magic directive and role....
Matthias Bussonnier -
Show More
@@ -1,7 +1,9 b''
1 1 .. _extensions_autoreload:
2 2
3 3 ==========
4 4 autoreload
5 5 ==========
6 6
7 .. magic:: autoreload
8
7 9 .. automodule:: IPython.extensions.autoreload
@@ -1,316 +1,317 b''
1 1 .. _autoawait:
2 2
3 3 Asynchronous in REPL: Autoawait
4 4 ===============================
5 5
6 6 .. note::
7 7
8 8 This feature is experimental and behavior can change between python and
9 9 IPython version without prior deprecation.
10 10
11 11 Starting with IPython 7.0, and when user Python 3.6 and above, IPython offer the
12 12 ability to run asynchronous code from the REPL. Constructs which are
13 13 :exc:`SyntaxError` s in the Python REPL can be used seamlessly in IPython.
14 14
15 15 The examples given here are for terminal IPython, running async code in a
16 16 notebook interface or any other frontend using the Jupyter protocol needs
17 17 IPykernel version 5.0 or above. The details of how async code runs in IPykernel
18 18 will differ between IPython, IPykernel and their versions.
19 19
20 20 When a supported library is used, IPython will automatically allow Futures and
21 21 Coroutines in the REPL to be ``await`` ed. This will happen if an :ref:`await
22 22 <await>` (or any other async constructs like async-with, async-for) is use at
23 23 top level scope, or if any structure valid only in `async def
24 24 <https://docs.python.org/3/reference/compound_stmts.html#async-def>`_ function
25 25 context are present. For example, the following being a syntax error in the
26 26 Python REPL::
27 27
28 28 Python 3.6.0
29 29 [GCC 4.2.1]
30 30 Type "help", "copyright", "credits" or "license" for more information.
31 31 >>> import aiohttp
32 32 >>> result = aiohttp.get('https://api.github.com')
33 33 >>> response = await result
34 34 File "<stdin>", line 1
35 35 response = await result
36 36 ^
37 37 SyntaxError: invalid syntax
38 38
39 39 Should behave as expected in the IPython REPL::
40 40
41 41 Python 3.6.0
42 42 Type 'copyright', 'credits' or 'license' for more information
43 43 IPython 7.0.0 -- An enhanced Interactive Python. Type '?' for help.
44 44
45 45 In [1]: import aiohttp
46 46 ...: result = aiohttp.get('https://api.github.com')
47 47
48 48 In [2]: response = await result
49 49 <pause for a few 100s ms>
50 50
51 51 In [3]: await response.json()
52 52 Out[3]:
53 53 {'authorizations_url': 'https://api.github.com/authorizations',
54 54 'code_search_url': 'https://api.github.com/search/code?q={query}...',
55 55 ...
56 56 }
57 57
58 58
59 59 You can use the ``c.InteractiveShell.autoawait`` configuration option and set it
60 60 to :any:`False` to deactivate automatic wrapping of asynchronous code. You can
61 also use the :magic:`%autoawait` magic to toggle the behavior at runtime::
61 also use the :magic:`autoawait` magic to toggle the behavior at runtime::
62 62
63 63 In [1]: %autoawait False
64 64
65 65 In [2]: %autoawait
66 66 IPython autoawait is `Off`, and set to use `asyncio`
67 67
68 68
69 69
70 70 By default IPython will assume integration with Python's provided
71 71 :mod:`asyncio`, but integration with other libraries is provided. In particular
72 72 we provide experimental integration with the ``curio`` and ``trio`` library.
73 73
74 74 You can switch current integration by using the
75 75 ``c.InteractiveShell.loop_runner`` option or the ``autoawait <name
76 76 integration>`` magic.
77 77
78 78 For example::
79 79
80 80 In [1]: %autoawait trio
81 81
82 82 In [2]: import trio
83 83
84 84 In [3]: async def child(i):
85 85 ...: print(" child %s goes to sleep"%i)
86 86 ...: await trio.sleep(2)
87 87 ...: print(" child %s wakes up"%i)
88 88
89 89 In [4]: print('parent start')
90 90 ...: async with trio.open_nursery() as n:
91 91 ...: for i in range(5):
92 92 ...: n.spawn(child, i)
93 93 ...: print('parent end')
94 94 parent start
95 95 child 2 goes to sleep
96 96 child 0 goes to sleep
97 97 child 3 goes to sleep
98 98 child 1 goes to sleep
99 99 child 4 goes to sleep
100 100 <about 2 seconds pause>
101 101 child 2 wakes up
102 102 child 1 wakes up
103 103 child 0 wakes up
104 104 child 3 wakes up
105 105 child 4 wakes up
106 106 parent end
107 107
108 108
109 109 In the above example, ``async with`` at top level scope is a syntax error in
110 110 Python.
111 111
112 112 Using this mode can have unexpected consequences if used in interaction with
113 113 other features of IPython and various registered extensions. In particular if
114 114 you are a direct or indirect user of the AST transformers, these may not apply
115 115 to your code.
116 116
117 117 When using command line IPython, the default loop (or runner) does not process
118 118 in the background, so top level asynchronous code must finish for the REPL to
119 119 allow you to enter more code. As with usual Python semantic, the awaitables are
120 120 started only when awaited for the first time. That is to say, in first example,
121 121 no network request is done between ``In[1]`` and ``In[2]``.
122 122
123 123
124 124 Effects on IPython.embed()
125 125 --------------------------
126 126
127 127 IPython core being asynchronous, the use of ``IPython.embed()`` will now require
128 128 a loop to run. By default IPython will use a fake coroutine runner which should
129 129 allow ``IPython.embed()`` to be nested. Though this will prevent usage of the
130 ``autoawait`` feature when using IPython embed.
130 :magic:`autoawait` feature when using IPython embed.
131 131
132 132 You can set explicitly a coroutine runner for ``embed()`` if you desire to run
133 133 asynchronous code, the exact behavior is though undefined.
134 134
135 135 Effects on Magics
136 136 -----------------
137 137
138 138 A couple of magics (``%%timeit``, ``%timeit``, ``%%time``, ``%%prun``) have not
139 139 yet been updated to work with asynchronous code and will raise syntax errors
140 140 when trying to use top-level ``await``. We welcome any contribution to help fix
141 141 those, and extra cases we haven't caught yet. We hope for better support in Cor
142 142 Python for top-level Async code.
143 143
144 144 Internals
145 145 ---------
146 146
147 147 As running asynchronous code is not supported in interactive REPL (as of Python
148 148 3.7) we have to rely to a number of complex workaround and heuristic to allow
149 149 this to happen. It is interesting to understand how this works in order to
150 150 comprehend potential bugs, or provide a custom runner.
151 151
152 152 Among the many approaches that are at our disposition, we find only one that
153 153 suited out need. Under the hood we use the code object from a async-def function
154 154 and run it in global namespace after modifying it to not create a new
155 155 ``locals()`` scope::
156 156
157 157 async def inner_async():
158 158 locals().update(**global_namespace)
159 159 #
160 160 # here is user code
161 161 #
162 162 return last_user_statement
163 163 codeobj = modify(inner_async.__code__)
164 164 coroutine = eval(codeobj, user_ns)
165 165 display(loop_runner(coroutine))
166 166
167 167
168 168
169 169 The first thing you'll notice is that unlike classical ``exec``, there is only
170 170 one namespace. Second, user code runs in a function scope, and not a module
171 171 scope.
172 172
173 173 On top of the above there are significant modification to the AST of
174 174 ``function``, and ``loop_runner`` can be arbitrary complex. So there is a
175 175 significant overhead to this kind of code.
176 176
177 177 By default the generated coroutine function will be consumed by Asyncio's
178 178 ``loop_runner = asyncio.get_evenloop().run_until_complete()`` method if
179 179 ``async`` mode is deemed necessary, otherwise the coroutine will just be
180 180 exhausted in a simple runner. It is though possible to change the default
181 181 runner.
182 182
183 183 A loop runner is a *synchronous* function responsible from running a coroutine
184 184 object.
185 185
186 186 The runner is responsible from ensuring that ``coroutine`` run to completion,
187 187 and should return the result of executing the coroutine. Let's write a
188 188 runner for ``trio`` that print a message when used as an exercise, ``trio`` is
189 189 special as it usually prefer to run a function object and make a coroutine by
190 190 itself, we can get around this limitation by wrapping it in an async-def without
191 191 parameters and passing this value to ``trio``::
192 192
193 193
194 194 In [1]: import trio
195 195 ...: from types import CoroutineType
196 196 ...:
197 197 ...: def trio_runner(coro:CoroutineType):
198 198 ...: print('running asynchronous code')
199 199 ...: async def corowrap(coro):
200 200 ...: return await coro
201 201 ...: return trio.run(corowrap, coro)
202 202
203 203 We can set it up by passing it to ``%autoawait``::
204 204
205 205 In [2]: %autoawait trio_runner
206 206
207 207 In [3]: async def async_hello(name):
208 208 ...: await trio.sleep(1)
209 209 ...: print(f'Hello {name} world !')
210 210 ...: await trio.sleep(1)
211 211
212 212 In [4]: await async_hello('async')
213 213 running asynchronous code
214 214 Hello async world !
215 215
216 216
217 217 Asynchronous programming in python (and in particular in the REPL) is still a
218 218 relatively young subject. We expect some code to not behave as you expect, so
219 219 feel free to contribute improvements to this codebase and give us feedback.
220 220
221 221 We invite you to thoroughly test this feature and report any unexpected behavior
222 222 as well as propose any improvement.
223 223
224 224 Using Autoawait in a notebook (IPykernel)
225 225 -----------------------------------------
226 226
227 227 Update ipykernel to version 5.0 or greater::
228 228
229 229 pip install ipykernel ipython --upgrade
230 230 # or
231 231 conda install ipykernel ipython --upgrade
232 232
233 This should automatically enable ``autoawait`` integration. Unlike terminal
234 IPython, all code runs on ``asyncio`` eventloop, so creating a loop by hand will
235 not work, including with magics like ``%run`` or other frameworks that create
236 the eventloop themselves. In cases like these you can try to use projects like
237 `nest_asyncio <https://github.com/erdewit/nest_asyncio>`_ and follow `this discussion
233 This should automatically enable :magic:`autoawait` integration. Unlike
234 terminal IPython, all code runs on ``asyncio`` eventloop, so creating a loop by
235 hand will not work, including with magics like :magic:`%run` or other
236 frameworks that create the eventloop themselves. In cases like these you can
237 try to use projects like `nest_asyncio
238 <https://github.com/erdewit/nest_asyncio>`_ and follow `this discussion
238 239 <https://github.com/jupyter/notebook/issues/3397#issuecomment-419386811>`_
239 240
240 241 Difference between terminal IPython and IPykernel
241 242 -------------------------------------------------
242 243
243 244 The exact asynchronous code running behavior varies between Terminal IPython and
244 245 IPykernel. The root cause of this behavior is due to IPykernel having a
245 *persistent* ``asyncio`` loop running, while Terminal IPython starts and stops a
246 *persistent* `asyncio` loop running, while Terminal IPython starts and stops a
246 247 loop for each code block. This can lead to surprising behavior in some case if
247 248 you are used to manipulate asyncio loop yourself, see for example
248 249 :ghissue:`11303` for a longer discussion but here are some of the astonishing
249 250 cases.
250 251
251 252 This behavior is an implementation detail, and should not be relied upon. It can
252 253 change without warnings in future versions of IPython.
253 254
254 255 In terminal IPython a loop is started for each code blocks only if there is top
255 256 level async code::
256 257
257 258 $ ipython
258 259 In [1]: import asyncio
259 260 ...: asyncio.get_event_loop()
260 261 Out[1]: <_UnixSelectorEventLoop running=False closed=False debug=False>
261 262
262 263 In [2]:
263 264
264 265 In [2]: import asyncio
265 266 ...: await asyncio.sleep(0)
266 267 ...: asyncio.get_event_loop()
267 268 Out[2]: <_UnixSelectorEventLoop running=True closed=False debug=False>
268 269
269 270 See that ``running`` is ``True`` only in the case were we ``await sleep()``
270 271
271 272 In a Notebook, with ipykernel the asyncio eventloop is always running::
272 273
273 274 $ jupyter notebook
274 275 In [1]: import asyncio
275 276 ...: loop1 = asyncio.get_event_loop()
276 277 ...: loop1
277 278 Out[1]: <_UnixSelectorEventLoop running=True closed=False debug=False>
278 279
279 280 In [2]: loop2 = asyncio.get_event_loop()
280 281 ...: loop2
281 282 Out[2]: <_UnixSelectorEventLoop running=True closed=False debug=False>
282 283
283 284 In [3]: loop1 is loop2
284 285 Out[3]: True
285 286
286 287 In Terminal IPython background tasks are only processed while the foreground
287 288 task is running, if and only if the foreground task is async::
288 289
289 290 $ ipython
290 291 In [1]: import asyncio
291 292 ...:
292 293 ...: async def repeat(msg, n):
293 294 ...: for i in range(n):
294 295 ...: print(f"{msg} {i}")
295 296 ...: await asyncio.sleep(1)
296 297 ...: return f"{msg} done"
297 298 ...:
298 299 ...: asyncio.ensure_future(repeat("background", 10))
299 300 Out[1]: <Task pending coro=<repeat() running at <ipython-input-1-02d0ef250fe7>:3>>
300 301
301 302 In [2]: await asyncio.sleep(3)
302 303 background 0
303 304 background 1
304 305 background 2
305 306 background 3
306 307
307 308 In [3]: import time
308 309 ...: time.sleep(5)
309 310
310 311 In [4]: await asyncio.sleep(3)
311 312 background 4
312 313 background 5
313 314 background 6g
314 315
315 316 In a Notebook, QtConsole, or any other frontend using IPykernel, background
316 317 tasks should behave as expected.
@@ -1,218 +1,225 b''
1 1 ============
2 2 7.x Series
3 3 ============
4 4
5 5 .. _whatsnew700:
6 6
7 7 IPython 7.0.0
8 8 =============
9 9
10 10 .. warning::
11 11
12 12 IPython 7.0 is currently in Beta. We welcome feedback on API/changes and
13 13 addition/updates to this changelog.
14 14
15 15 Released .... ...., 2018
16 16
17 17 IPython 7 include major features improvement as you can read in the following
18 18 changelog. This is also the second major version of IPython to support only
19 19 Python 3 – starting at Python 3.4. Python 2 is still community supported
20 20 on the bugfix only 5.x branch, but we remind you that Python 2 "end of life"
21 21 is on Jan 1st 2020.
22 22
23 23 We were able to backport bug fixes to the 5.x branch thanks to our backport bot which
24 24 backported more than `70 Pull-Requests
25 25 <https://github.com/ipython/ipython/pulls?page=3&q=is%3Apr+sort%3Aupdated-desc+author%3Aapp%2Fmeeseeksdev++5.x&utf8=%E2%9C%93>`_, but there are still many PRs that required manually work, and this is an area of the project were you can easily contribute by looking for `PRs still needed backport <https://github.com/ipython/ipython/issues?q=label%3A%22Still+Needs+Manual+Backport%22+is%3Aclosed+sort%3Aupdated-desc>`_
26 26
27 27 IPython 6.x branch will likely not see any further release unless critical
28 28 bugs are found.
29 29
30 30 Make sure you have pip > 9.0 before upgrading. You should be able to update by simply running
31 31
32 32 .. code::
33 33
34 34 pip install ipython --upgrade
35 35
36 36 Or if you have conda installed:
37 37
38 38 .. code::
39 39
40 40 conda install ipython
41 41
42 42
43 43
44 44 Prompt Toolkit 2.0
45 45 ------------------
46 46
47 47 IPython 7.0+ now uses ``prompt_toolkit 2.0``, if you still need to use earlier
48 48 ``prompt_toolkit`` version you may need to pin IPython to ``<7.0``.
49 49
50 50 Autowait: Asynchronous REPL
51 51 ---------------------------
52 52
53 53 Staring with IPython 7.0 and on Python 3.6+, IPython can automatically await
54 54 code at top level, you should not need to access an event loop or runner
55 55 yourself. To know more read the :ref:`autoawait` section of our docs, see
56 56 :ghpull:`11265` or try the following code::
57 57
58 58 Python 3.6.0
59 59 Type 'copyright', 'credits' or 'license' for more information
60 60 IPython 7.0.0 -- An enhanced Interactive Python. Type '?' for help.
61 61
62 62 In [1]: import aiohttp
63 63 ...: result = aiohttp.get('https://api.github.com')
64 64
65 65 In [2]: response = await result
66 66 <pause for a few 100s ms>
67 67
68 68 In [3]: await response.json()
69 69 Out[3]:
70 70 {'authorizations_url': 'https://api.github.com/authorizations',
71 71 'code_search_url': 'https://api.github.com/search/code?q={query}{&page,per_page,sort,order}',
72 72 ...
73 73 }
74 74
75 75 .. note::
76 76
77 77 Async integration is experimental code, behavior may change or be removed
78 78 between Python and IPython versions without warnings.
79 79
80 80 Integration is by default with `asyncio`, but other libraries can be configured,
81 81 like ``curio`` or ``trio``, to improve concurrency in the REPL::
82 82
83 83 In [1]: %autoawait trio
84 84
85 85 In [2]: import trio
86 86
87 87 In [3]: async def child(i):
88 88 ...: print(" child %s goes to sleep"%i)
89 89 ...: await trio.sleep(2)
90 90 ...: print(" child %s wakes up"%i)
91 91
92 92 In [4]: print('parent start')
93 93 ...: async with trio.open_nursery() as n:
94 94 ...: for i in range(3):
95 95 ...: n.spawn(child, i)
96 96 ...: print('parent end')
97 97 parent start
98 98 child 2 goes to sleep
99 99 child 0 goes to sleep
100 100 child 1 goes to sleep
101 101 <about 2 seconds pause>
102 102 child 2 wakes up
103 103 child 1 wakes up
104 104 child 0 wakes up
105 105 parent end
106 106
107 107 See :ref:`autoawait` for more information.
108 108
109 109
110 110 Asynchronous code in a Notebook interface or any other frontend using the
111 111 Jupyter Protocol will need further updates of the IPykernel package.
112 112
113 113 Non-Asynchronous code
114 114 ~~~~~~~~~~~~~~~~~~~~~
115 115
116 116 As the internal API of IPython are now asynchronous, IPython need to run under
117 an even loop. In order to allow many workflow, (like using the ``%run`` magic,
118 or copy_pasting code that explicitly starts/stop event loop), when top-level code
119 is detected as not being asynchronous, IPython code is advanced via a
120 pseudo-synchronous runner, and will not may not advance pending tasks.
117 an even loop. In order to allow many workflow, (like using the :magic:`%run`
118 magic, or copy_pasting code that explicitly starts/stop event loop), when
119 top-level code is detected as not being asynchronous, IPython code is advanced
120 via a pseudo-synchronous runner, and will not may not advance pending tasks.
121 121
122 122 Change to Nested Embed
123 123 ~~~~~~~~~~~~~~~~~~~~~~
124 124
125 125 The introduction of the ability to run async code had some effect on the
126 126 ``IPython.embed()`` API. By default embed will not allow you to run asynchronous
127 127 code unless a event loop is specified.
128 128
129 129 Effects on Magics
130 130 ~~~~~~~~~~~~~~~~~
131 131
132 132 Some magics will not work with Async, and will need updates. Contribution
133 133 welcome.
134 134
135 135 Expected Future changes
136 136 ~~~~~~~~~~~~~~~~~~~~~~~
137 137
138 138 We expect more internal but public IPython function to become ``async``, and
139 139 will likely end up having a persisting event loop while IPython is running.
140 140
141 141 Thanks
142 142 ~~~~~~
143 143
144 144 This took more than a year in the making, and the code was rebased a number of
145 145 time leading to commit authorship that may have been lost in the final
146 146 Pull-Request. Huge thanks to many people for contribution, discussion, code,
147 147 documentation, use-case: dalejung, danielballan, ellisonbg, fperez, gnestor,
148 148 minrk, njsmith, pganssle, tacaswell, takluyver , vidartf ... And many others.
149 149
150 150
151 151 Autoreload Improvement
152 152 ----------------------
153 153
154 The magic ``%autoreload 2`` now captures new methods added to classes. Earlier, only methods existing as of the initial import were being tracked and updated.
154 The magic :magic:`%autoreload 2 <autoreload>` now captures new methods added to
155 classes. Earlier, only methods existing as of the initial import were being
156 tracked and updated.
155 157
156 This new feature helps dual environment development - Jupyter+IDE - where the code gradually moves from notebook cells to package files, as it gets structured.
158 This new feature helps dual environment development - Jupyter+IDE - where the
159 code gradually moves from notebook cells to package files, as it gets
160 structured.
157 161
158 **Example**: An instance of the class `MyClass` will be able to access the method `cube()` after it is uncommented and the file `file1.py` saved on disk.
162 **Example**: An instance of the class ``MyClass`` will be able to access the
163 method ``cube()`` after it is uncommented and the file ``file1.py`` saved on
164 disk.
159 165
160 166
161 167 ..code::
162 168
163 169 # notebook
164 170
165 171 from mymodule import MyClass
166 172 first = MyClass(5)
167 173
168 174 .. code::
169 175
170 176 # mymodule/file1.py
171 177
172 178 class MyClass:
173 179
174 180 def __init__(self, a=10):
175 181 self.a = a
176 182
177 183 def square(self):
178 184 print('compute square')
179 185 return self.a*self.a
180 186
181 187 # def cube(self):
182 188 # print('compute cube')
183 189 # return self.a*self.a*self.a
184 190
185 191
186 192
187 193
188 194 Misc
189 195 ----
190 196
191 197 The autoindent feature that was deprecated in 5.x was re-enabled and
192 198 un-deprecated in :ghpull:`11257`
193 199
194 Make ``%run -n -i ...`` work correctly. Earlier, if ``%run`` was passed both arguments, ``-n`` would be silently ignored. See :ghpull:`10308`
200 Make :magic:`%run -n -i ... <run>` work correctly. Earlier, if :magic:`%run` was
201 passed both arguments, ``-n`` would be silently ignored. See :ghpull:`10308`
195 202
196 203
197 The ``%%script`` (as well as ``%%bash``, ``ruby``... ) cell magics now raise
198 by default if the return code of the given code is non-zero (thus halting
199 execution of further cells in a notebook). The behavior can be disable by
200 passing the ``--no-raise-error`` flag.
204 The :cellmagic:`%%script`` (as well as :cellmagic:`%%bash``,
205 :cellmagic:`%%ruby``... ) cell magics now raise by default if the return code of
206 the given code is non-zero (thus halting execution of further cells in a
207 notebook). The behavior can be disable by passing the ``--no-raise-error`` flag.
201 208
202 209
203 210 Deprecations
204 211 ------------
205 212
206 213 A couple of unused function and methods have been deprecated and will be removed
207 214 in future versions:
208 215
209 216 - ``IPython.utils.io.raw_print_err``
210 217 - ``IPython.utils.io.raw_print``
211 218
212 219
213 220 Backwards incompatible changes
214 221 ------------------------------
215 222
216 223 * The API for transforming input before it is parsed as Python code has been
217 224 completely redesigned, and any custom input transformations will need to be
218 225 rewritten. See :doc:`/config/inputtransforms` for details of the new API.
@@ -1,157 +1,160 b''
1 1 """Define text roles for GitHub
2 2
3 3 * ghissue - Issue
4 4 * ghpull - Pull Request
5 5 * ghuser - User
6 6
7 7 Adapted from bitbucket example here:
8 8 https://bitbucket.org/birkenfeld/sphinx-contrib/src/tip/bitbucket/sphinxcontrib/bitbucket.py
9 9
10 10 Authors
11 11 -------
12 12
13 13 * Doug Hellmann
14 14 * Min RK
15 15 """
16 16 #
17 17 # Original Copyright (c) 2010 Doug Hellmann. All rights reserved.
18 18 #
19 19
20 20 from docutils import nodes, utils
21 21 from docutils.parsers.rst.roles import set_classes
22 from sphinx.util.logging import getLogger
23
24 info = getLogger(__name__).info
22 25
23 26 def make_link_node(rawtext, app, type, slug, options):
24 27 """Create a link to a github resource.
25 28
26 29 :param rawtext: Text being replaced with link node.
27 30 :param app: Sphinx application context
28 31 :param type: Link type (issues, changeset, etc.)
29 32 :param slug: ID of the thing to link to
30 33 :param options: Options dictionary passed to role func.
31 34 """
32 35
33 36 try:
34 37 base = app.config.github_project_url
35 38 if not base:
36 39 raise AttributeError
37 40 if not base.endswith('/'):
38 41 base += '/'
39 42 except AttributeError as err:
40 43 raise ValueError('github_project_url configuration value is not set (%s)' % str(err))
41 44
42 45 ref = base + type + '/' + slug + '/'
43 46 set_classes(options)
44 47 prefix = "#"
45 48 if type == 'pull':
46 49 prefix = "PR " + prefix
47 50 node = nodes.reference(rawtext, prefix + utils.unescape(slug), refuri=ref,
48 51 **options)
49 52 return node
50 53
51 54 def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
52 55 """Link to a GitHub issue.
53 56
54 57 Returns 2 part tuple containing list of nodes to insert into the
55 58 document and a list of system messages. Both are allowed to be
56 59 empty.
57 60
58 61 :param name: The role name used in the document.
59 62 :param rawtext: The entire markup snippet, with role.
60 63 :param text: The text marked with the role.
61 64 :param lineno: The line number where rawtext appears in the input.
62 65 :param inliner: The inliner instance that called us.
63 66 :param options: Directive options for customization.
64 67 :param content: The directive content for customization.
65 68 """
66 69
67 70 try:
68 71 issue_num = int(text)
69 72 if issue_num <= 0:
70 73 raise ValueError
71 74 except ValueError:
72 75 msg = inliner.reporter.error(
73 76 'GitHub issue number must be a number greater than or equal to 1; '
74 77 '"%s" is invalid.' % text, line=lineno)
75 78 prb = inliner.problematic(rawtext, rawtext, msg)
76 79 return [prb], [msg]
77 80 app = inliner.document.settings.env.app
78 #app.info('issue %r' % text)
81 #info('issue %r' % text)
79 82 if 'pull' in name.lower():
80 83 category = 'pull'
81 84 elif 'issue' in name.lower():
82 85 category = 'issues'
83 86 else:
84 87 msg = inliner.reporter.error(
85 88 'GitHub roles include "ghpull" and "ghissue", '
86 89 '"%s" is invalid.' % name, line=lineno)
87 90 prb = inliner.problematic(rawtext, rawtext, msg)
88 91 return [prb], [msg]
89 92 node = make_link_node(rawtext, app, category, str(issue_num), options)
90 93 return [node], []
91 94
92 95 def ghuser_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
93 96 """Link to a GitHub user.
94 97
95 98 Returns 2 part tuple containing list of nodes to insert into the
96 99 document and a list of system messages. Both are allowed to be
97 100 empty.
98 101
99 102 :param name: The role name used in the document.
100 103 :param rawtext: The entire markup snippet, with role.
101 104 :param text: The text marked with the role.
102 105 :param lineno: The line number where rawtext appears in the input.
103 106 :param inliner: The inliner instance that called us.
104 107 :param options: Directive options for customization.
105 108 :param content: The directive content for customization.
106 109 """
107 110 app = inliner.document.settings.env.app
108 #app.info('user link %r' % text)
111 #info('user link %r' % text)
109 112 ref = 'https://www.github.com/' + text
110 113 node = nodes.reference(rawtext, text, refuri=ref, **options)
111 114 return [node], []
112 115
113 116 def ghcommit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
114 117 """Link to a GitHub commit.
115 118
116 119 Returns 2 part tuple containing list of nodes to insert into the
117 120 document and a list of system messages. Both are allowed to be
118 121 empty.
119 122
120 123 :param name: The role name used in the document.
121 124 :param rawtext: The entire markup snippet, with role.
122 125 :param text: The text marked with the role.
123 126 :param lineno: The line number where rawtext appears in the input.
124 127 :param inliner: The inliner instance that called us.
125 128 :param options: Directive options for customization.
126 129 :param content: The directive content for customization.
127 130 """
128 131 app = inliner.document.settings.env.app
129 #app.info('user link %r' % text)
132 #info('user link %r' % text)
130 133 try:
131 134 base = app.config.github_project_url
132 135 if not base:
133 136 raise AttributeError
134 137 if not base.endswith('/'):
135 138 base += '/'
136 139 except AttributeError as err:
137 140 raise ValueError('github_project_url configuration value is not set (%s)' % str(err))
138 141
139 142 ref = base + text
140 143 node = nodes.reference(rawtext, text[:6], refuri=ref, **options)
141 144 return [node], []
142 145
143 146
144 147 def setup(app):
145 148 """Install the plugin.
146 149
147 150 :param app: Sphinx application context.
148 151 """
149 app.info('Initializing GitHub plugin')
152 info('Initializing GitHub plugin')
150 153 app.add_role('ghissue', ghissue_role)
151 154 app.add_role('ghpull', ghissue_role)
152 155 app.add_role('ghuser', ghuser_role)
153 156 app.add_role('ghcommit', ghcommit_role)
154 157 app.add_config_value('github_project_url', None, 'env')
155 158
156 159 metadata = {'parallel_read_safe': True, 'parallel_write_safe': True}
157 160 return metadata
@@ -1,45 +1,46 b''
1 1 import re
2 2 from sphinx import addnodes
3 3 from sphinx.domains.std import StandardDomain
4 4 from sphinx.roles import XRefRole
5 5
6 6 name_re = re.compile(r"[\w_]+")
7 7
8 8 def parse_magic(env, sig, signode):
9 9 m = name_re.match(sig)
10 10 if not m:
11 11 raise Exception("Invalid magic command: %s" % sig)
12 12 name = "%" + sig
13 13 signode += addnodes.desc_name(name, name)
14 14 return m.group(0)
15 15
16 16 class LineMagicRole(XRefRole):
17 17 """Cross reference role displayed with a % prefix"""
18 18 prefix = "%"
19 19
20 20 def process_link(self, env, refnode, has_explicit_title, title, target):
21 21 if not has_explicit_title:
22 22 title = self.prefix + title.lstrip("%")
23 23 target = target.lstrip("%")
24 24 return title, target
25 25
26 26 def parse_cell_magic(env, sig, signode):
27 27 m = name_re.match(sig)
28 28 if not m:
29 29 raise ValueError("Invalid cell magic: %s" % sig)
30 30 name = "%%" + sig
31 31 signode += addnodes.desc_name(name, name)
32 32 return m.group(0)
33 33
34 34 class CellMagicRole(LineMagicRole):
35 35 """Cross reference role displayed with a %% prefix"""
36 36 prefix = "%%"
37 37
38 38 def setup(app):
39 39 app.add_object_type('magic', 'magic', 'pair: %s; magic command', parse_magic)
40 StandardDomain.roles['magic'] = LineMagicRole()
40 app.add_role_to_domain('std', 'magic', LineMagicRole(), override=True)
41
41 42 app.add_object_type('cellmagic', 'cellmagic', 'pair: %s; cell magic', parse_cell_magic)
42 StandardDomain.roles['cellmagic'] = CellMagicRole()
43 app.add_role_to_domain('std', 'cellmagic', CellMagicRole(), override=True)
43 44
44 45 metadata = {'parallel_read_safe': True, 'parallel_write_safe': True}
45 46 return metadata
General Comments 0
You need to be logged in to leave comments. Login now