Merge lp:~adiroiban/pydoctor/1318325-intersphinx into lp:~mwhudson/pydoctor/dev
- 1318325-intersphinx
- Merge into dev
Status: | Merged | ||||
---|---|---|---|---|---|
Merge reported by: | Michael Hudson-Doyle | ||||
Merged at revision: | not available | ||||
Proposed branch: | lp:~adiroiban/pydoctor/1318325-intersphinx | ||||
Merge into: | lp:~mwhudson/pydoctor/dev | ||||
Diff against target: |
994 lines (+839/-34) (has conflicts) 8 files modified
README.txt (+68/-30) pydoctor/driver.py (+11/-0) pydoctor/epydoc2stan.py (+13/-0) pydoctor/model.py (+23/-4) pydoctor/sphinx.py (+187/-0) pydoctor/test/test_epydoc2stan.py (+52/-0) pydoctor/test/test_model.py (+79/-0) pydoctor/test/test_sphinx.py (+406/-0) Text conflict in README.txt Conflict adding file pydoctor/sphinx.py. Moved existing file to pydoctor/sphinx.py.moved. Conflict adding file pydoctor/test/test_sphinx.py. Moved existing file to pydoctor/test/test_sphinx.py.moved. |
||||
To merge this branch: | bzr merge lp:~adiroiban/pydoctor/1318325-intersphinx | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Hudson-Doyle | Pending | ||
Review via email: mp+219128@code.launchpad.net |
Commit message
Description of the change
Problem description
-------------------
pydoctor has an hardcoded links for stdlibs but it would be nice to be able
to link to sphinx apidocs using Sphinx inventory files
Changes
-------
Added '--intersphinx' configuration option.
It assumes that a remote object inventory contains indexed for a single root package.
This is why sphinx:http://
System was updated to allow initialization with options and to download and parse requested objects inventory.
Intersphinx inventories are stored using a simple dict and they provide a getLink method.
epydoc2stan linker was updated to look for intersphinx links when previous lookups fail.
How to test
-----------
Check that README documentation make sense... not sure where to put this
info.
I have also done a simple manual test using sphinx object.inv from rtd.org
Set one of the following markups
def getparser():
"""
Return parser for command line options.
L{pretty <sphinx.
"""
or
def getparser():
"""
Return parser for command line options.
L{sphinx.
"""
build pydoctor using
pydoctor --intersphinx=
I tried to write as much test as possible but this is a weekend hack and there might be gaps in coverage.. or errors in code.
Please check that changes make sense.
Let me know what needs to be changes or if you have better idea for how
this could be implemented.
I don't have to much experience with pydoctor usage and how people expect
to link to 3rd party.
Thanks!
Michael Hudson-Doyle (mwhudson) wrote : | # |
Adi Roiban (adiroiban) wrote : | # |
a) For cache part, I prefer to do it in a separate ticket to keep this diff small.
b) I don't know what do you want from this ... can you please add more details?
Thanks!
Michael Hudson-Doyle (mwhudson) wrote : | # |
Adi Roiban <email address hidden> writes:
> a) For cache part, I prefer to do it in a separate ticket to keep this diff small.
+1. I also wonder about automatically appending objects.inv to the
supplied URL but well, that can wait as well.
> b) I don't know what do you want from this ... can you please add more details?
Well, all I mean is that it would be nice if Twisted could somehow say
that you should use the objects.inv from the stdlib, pyasn1 and
pycrypto, or whatever, rather than everyone who generates documentation
having to remember to pass them each time.
Having looked at intersphinx's documentation a bit, I think that it
should work a bit differently from this branch. In particular, I don't
think that naming the objects.inv really makes sense for pydoctor -- I
think we should just take a list of them and consult them in order.
But I tested it and it works, so thanks!
Michael Hudson-Doyle (mwhudson) wrote : | # |
I pushed a change to lp:~mwhudson/pydoctor/1318325-intersphinx to search through all inventories. (it still makes you name them)
It generates a lot more invalid refs than trunk but afaict trunk is generating broken links for all of them...
Adi Roiban (adiroiban) wrote : | # |
Appending objects.inv could be added later :) ... this is a tool for developers and I assume all command arguments are only written once and then integrated into the build system.
I prefer to have explicit settings and know what URL is used.
-------
b) ... ok but then at least for stdlib we also need to configure if this is py2 or py3 ... maybe based on current python version.
I would leave this for a new ticket so that people start using the current code and then send some feedback.
Also, I see that pydoctor has a hack to load configuration options from a file... one workaround is to provide a sample
From my point of view intersphinx works in a similar way, as intershpinx does not arbitrary search all configured objects.inv
intersphinx_mapping = {
'py2': ('http://
'py3': ('http://
}
Adapted from documentation
A link like :ref:`comparison manual <py2:os.path>` will link to the label “os.path” in the doc set “py2”, if it exists.
To have a similar markup as intersphinx, we should need to use L{py2:os.path} instead of L{os.path}
I agree that for pydoctor this does not make sense and is ok to search whole list.
In this case --intersphinx http://
---
Maybe we can merge this branch is is and then have a new ticket to update code to look in all objects.inv and a new ticket to update stdlib links .... and see what to do with py2 vs py3 for stdlib.
..
Thanks!
Michael Hudson-Doyle (mwhudson) wrote : | # |
Adi Roiban <email address hidden> writes:
> Appending objects.inv could be added later :) ... this is a tool for developers and I assume all command arguments are only written once and then integrated into the build system.
>
> I prefer to have explicit settings and know what URL is used.
Fair enough.
> -------
>
> b) ... ok but then at least for stdlib we also need to configure if this is py2 or py3 ... maybe based on current python version.
Well, pydoctor is python 2 only currently. I guess you could use it to
document something written in the intersection of py2 and py3, but I'd
be very surprised if it ran under py3...
> I would leave this for a new ticket so that people start using the current code and then send some feedback.
>
> Also, I see that pydoctor has a hack to load configuration options from a file... one workaround is to provide a sample
>
>>From my point of view intersphinx works in a similar way, as intershpinx does not arbitrary search all configured objects.inv
>
> intersphinx_mapping = {
> 'py2': ('http://
> 'py3': ('http://
> }
>
> Adapted from documentation
>
> A link like :ref:`comparison manual <py2:os.path>` will link to the label “os.path” in the doc set “py2”, if it exists.
>
> To have a similar markup as intersphinx, we should need to use L{py2:os.path} instead of L{os.path}
>
> I agree that for pydoctor this does not make sense and is ok to search whole list.
>
> In this case --intersphinx http://
Heh, I think you just went through the same thought process I did. If
we supported things like :ref: links it would make sense -- but we
don't, it's more like the :py:class: stuff.
> ---
>
> Maybe we can merge this branch is is and then have a new ticket to update code to look in all objects.inv and a new ticket to update stdlib links .... and see what to do with py2 vs py3 for stdlib.
Yeah, that seems fair. I think I might just check the objects.inv for
python (maybe both 2 and 3) into pydoctor and refer to them by
default... but yeah, let's get something simple in first.
Cheers,
mwh
Adi Roiban (adiroiban) wrote : | # |
I have updated the code so that intersphinx only requires a full url to objects.inv files.
The lookup for intersphinx is done from a single database.
If this is ok, I can create a new ticket/branch to automatically populate the inventory with python2.7 object.inv ... but I find it of a lower priority...
The next step would be to cache downloaded intersphinx files.
Please check the latest code.
Adi Roiban (adiroiban) wrote : | # |
Hm.. looks like the branch has conflicts.
I have merged master branch into this and fix conflicts with git-bzr ... but looks like they were not applied to bzr :( just use code from latest branch as it is merged with master.
Thanks!
- 618. By Adi Roiban <email address hidden>
-
Add tests.
Michael Hudson-Doyle (mwhudson) wrote : | # |
Well fiiiiiiiiiiiiii
Preview Diff
1 | === modified file 'README.txt' | |||
2 | --- README.txt 2014-05-11 11:09:03 +0000 | |||
3 | +++ README.txt 2014-05-30 08:40:38 +0000 | |||
4 | @@ -14,33 +14,71 @@ | |||
5 | 14 | The default HTML generator requires Twisted. | 14 | The default HTML generator requires Twisted. |
6 | 15 | 15 | ||
7 | 16 | There are some more notes in the doc/ subdirectory. | 16 | There are some more notes in the doc/ subdirectory. |
38 | 17 | 17 | <<<<<<< TREE | |
39 | 18 | 18 | ||
40 | 19 | Sphinx Integration | 19 | |
41 | 20 | ------------------ | 20 | Sphinx Integration |
42 | 21 | 21 | ------------------ | |
43 | 22 | HTML generator will also generate a Sphinx objects inventory using the | 22 | |
44 | 23 | following mapping: | 23 | HTML generator will also generate a Sphinx objects inventory using the |
45 | 24 | 24 | following mapping: | |
46 | 25 | * packages, modules -> py:mod: | 25 | |
47 | 26 | * classes -> py:class: | 26 | * packages, modules -> py:mod: |
48 | 27 | * functions -> py:func: | 27 | * classes -> py:class: |
49 | 28 | * methods -> py:meth: | 28 | * functions -> py:func: |
50 | 29 | * attributes -> py:attr: | 29 | * methods -> py:meth: |
51 | 30 | 30 | * attributes -> py:attr: | |
52 | 31 | Configure Sphinx intersphinx extension: | 31 | |
53 | 32 | 32 | Configure Sphinx intersphinx extension: | |
54 | 33 | intersphinx_mapping = { | 33 | |
55 | 34 | 'pydoctor': ('http://domain.tld/api', None), | 34 | intersphinx_mapping = { |
56 | 35 | } | 35 | 'pydoctor': ('http://domain.tld/api', None), |
57 | 36 | 36 | } | |
58 | 37 | Use external references:: | 37 | |
59 | 38 | 38 | Use external references:: | |
60 | 39 | :py:func:`External API <pydoctor:pydoctor.model.Documentable.reparent>` | 39 | |
61 | 40 | 40 | :py:func:`External API <pydoctor:pydoctor.model.Documentable.reparent>` | |
62 | 41 | :py:mod:`pydoctor:pydoctor` | 41 | |
63 | 42 | :py:mod:`pydoctor:pydoctor.model` | 42 | :py:mod:`pydoctor:pydoctor` |
64 | 43 | :py:func:`pydoctor:pydoctor.driver.getparser` | 43 | :py:mod:`pydoctor:pydoctor.model` |
65 | 44 | :py:class:`pydoctor:pydoctor.model.Documentable` | 44 | :py:func:`pydoctor:pydoctor.driver.getparser` |
66 | 45 | :py:meth:`pydoctor:pydoctor.model.Documentable.reparent` | 45 | :py:class:`pydoctor:pydoctor.model.Documentable` |
67 | 46 | :py:attr:`pydoctor:pydoctor.model.Documentable.kind` | 46 | :py:meth:`pydoctor:pydoctor.model.Documentable.reparent` |
68 | 47 | :py:attr:`pydoctor:pydoctor.model.Documentable.kind` | ||
69 | 48 | ======= | ||
70 | 49 | |||
71 | 50 | |||
72 | 51 | Sphinx Integration | ||
73 | 52 | ------------------ | ||
74 | 53 | |||
75 | 54 | HTML generator will also generate a Sphinx objects inventory using the | ||
76 | 55 | following mapping: | ||
77 | 56 | |||
78 | 57 | * packages, modules -> py:mod: | ||
79 | 58 | * classes -> py:class: | ||
80 | 59 | * functions -> py:func: | ||
81 | 60 | * methods -> py:meth: | ||
82 | 61 | * attributes -> py:attr: | ||
83 | 62 | |||
84 | 63 | Configure Sphinx intersphinx extension: | ||
85 | 64 | |||
86 | 65 | intersphinx_mapping = { | ||
87 | 66 | 'pydoctor': ('http://domain.tld/api', None), | ||
88 | 67 | } | ||
89 | 68 | |||
90 | 69 | Use external references:: | ||
91 | 70 | |||
92 | 71 | :py:func:`External API <pydoctor:pydoctor.model.Documentable.reparent>` | ||
93 | 72 | |||
94 | 73 | :py:mod:`pydoctor:pydoctor` | ||
95 | 74 | :py:mod:`pydoctor:pydoctor.model` | ||
96 | 75 | :py:func:`pydoctor:pydoctor.driver.getparser` | ||
97 | 76 | :py:class:`pydoctor:pydoctor.model.Documentable` | ||
98 | 77 | :py:meth:`pydoctor:pydoctor.model.Documentable.reparent` | ||
99 | 78 | :py:attr:`pydoctor:pydoctor.model.Documentable.kind` | ||
100 | 79 | |||
101 | 80 | It can link to external API documentation using Sphinx objects inventory using | ||
102 | 81 | the following cumulative configuration option:: | ||
103 | 82 | |||
104 | 83 | --intersphinx=http://sphinx.org/objects.inv | ||
105 | 84 | >>>>>>> MERGE-SOURCE | ||
106 | 47 | 85 | ||
107 | === modified file 'pydoctor/driver.py' | |||
108 | --- pydoctor/driver.py 2014-05-13 08:46:30 +0000 | |||
109 | +++ pydoctor/driver.py 2014-05-30 08:40:38 +0000 | |||
110 | @@ -186,6 +186,14 @@ | |||
111 | 186 | parser.add_option( | 186 | parser.add_option( |
112 | 187 | '--introspect-c-modules', default=False, action='store_true', | 187 | '--introspect-c-modules', default=False, action='store_true', |
113 | 188 | help=("Import and introspect any C modules found.")) | 188 | help=("Import and introspect any C modules found.")) |
114 | 189 | |||
115 | 190 | parser.add_option( | ||
116 | 191 | '--intersphinx', action='append', dest='intersphinx', | ||
117 | 192 | metavar='URL_TO_OBJECTS.INV', default=[], | ||
118 | 193 | help=( | ||
119 | 194 | "Use Sphinx objects inventory to generate links to external" | ||
120 | 195 | "documetation. Can be repeated.")) | ||
121 | 196 | |||
122 | 189 | return parser | 197 | return parser |
123 | 190 | 198 | ||
124 | 191 | def readConfigFile(options): | 199 | def readConfigFile(options): |
125 | @@ -252,7 +260,10 @@ | |||
126 | 252 | else: | 260 | else: |
127 | 253 | system = systemclass() | 261 | system = systemclass() |
128 | 254 | 262 | ||
129 | 263 | # Once pickle support is removed, always instantiate System with | ||
130 | 264 | # options and make fetchIntersphinxInventories private in __init__. | ||
131 | 255 | system.options = options | 265 | system.options = options |
132 | 266 | system.fetchIntersphinxInventories() | ||
133 | 256 | 267 | ||
134 | 257 | system.urlprefix = '' | 268 | system.urlprefix = '' |
135 | 258 | if options.moresystems: | 269 | if options.moresystems: |
136 | 259 | 270 | ||
137 | === modified file 'pydoctor/epydoc2stan.py' | |||
138 | --- pydoctor/epydoc2stan.py 2014-01-06 20:37:07 +0000 | |||
139 | +++ pydoctor/epydoc2stan.py 2014-05-30 08:40:38 +0000 | |||
140 | @@ -113,6 +113,14 @@ | |||
141 | 113 | thresh=-1) | 113 | thresh=-1) |
142 | 114 | return None | 114 | return None |
143 | 115 | 115 | ||
144 | 116 | def look_for_intersphinx(self, name): | ||
145 | 117 | """ | ||
146 | 118 | Return link for `name` based on intersphinx inventory. | ||
147 | 119 | |||
148 | 120 | Return None if link is not found. | ||
149 | 121 | """ | ||
150 | 122 | return self.obj.system.intersphinx.getLink(name) | ||
151 | 123 | |||
152 | 116 | def translate_identifier_xref(self, fullID, prettyID): | 124 | def translate_identifier_xref(self, fullID, prettyID): |
153 | 117 | """Figure out what ``L{fullID}`` should link to. | 125 | """Figure out what ``L{fullID}`` should link to. |
154 | 118 | 126 | ||
155 | @@ -163,6 +171,11 @@ | |||
156 | 163 | self.obj.system.objectsOfType(model.Package))) | 171 | self.obj.system.objectsOfType(model.Package))) |
157 | 164 | if target is not None: | 172 | if target is not None: |
158 | 165 | return self._objLink(target, prettyID) | 173 | return self._objLink(target, prettyID) |
159 | 174 | |||
160 | 175 | target = self.look_for_intersphinx(fullID) | ||
161 | 176 | if target: | ||
162 | 177 | return '<a href="%s"><code>%s</code></a>'%(target, prettyID) | ||
163 | 178 | |||
164 | 166 | self.obj.system.msg( | 179 | self.obj.system.msg( |
165 | 167 | "translate_identifier_xref", "%s:%s invalid ref to %s" % ( | 180 | "translate_identifier_xref", "%s:%s invalid ref to %s" % ( |
166 | 168 | self.obj.fullName(), self.obj.linenumber, fullID), | 181 | self.obj.fullName(), self.obj.linenumber, fullID), |
167 | 169 | 182 | ||
168 | === modified file 'pydoctor/model.py' | |||
169 | --- pydoctor/model.py 2014-05-13 08:46:30 +0000 | |||
170 | +++ pydoctor/model.py 2014-05-30 08:40:38 +0000 | |||
171 | @@ -14,6 +14,8 @@ | |||
172 | 14 | import types | 14 | import types |
173 | 15 | import __builtin__ | 15 | import __builtin__ |
174 | 16 | 16 | ||
175 | 17 | from pydoctor.sphinx import SphinxInventory | ||
176 | 18 | |||
177 | 17 | # originally when I started to write pydoctor I had this idea of a big | 19 | # originally when I started to write pydoctor I had this idea of a big |
178 | 18 | # tree of Documentables arranged in an almost arbitrary tree. | 20 | # tree of Documentables arranged in an almost arbitrary tree. |
179 | 19 | # | 21 | # |
180 | @@ -347,7 +349,7 @@ | |||
181 | 347 | #defaultBuilder = astbuilder.ASTBuilder | 349 | #defaultBuilder = astbuilder.ASTBuilder |
182 | 348 | sourcebase = None | 350 | sourcebase = None |
183 | 349 | 351 | ||
185 | 350 | def __init__(self): | 352 | def __init__(self, options=None): |
186 | 351 | self.allobjects = {} | 353 | self.allobjects = {} |
187 | 352 | self.orderedallobjects = [] | 354 | self.orderedallobjects = [] |
188 | 353 | self.rootobjects = [] | 355 | self.rootobjects = [] |
189 | @@ -356,9 +358,14 @@ | |||
190 | 356 | self.moresystems = [] | 358 | self.moresystems = [] |
191 | 357 | self.subsystems = [] | 359 | self.subsystems = [] |
192 | 358 | self.urlprefix = '' | 360 | self.urlprefix = '' |
196 | 359 | from pydoctor.driver import parse_args | 361 | |
197 | 360 | self.options, _ = parse_args([]) | 362 | if options: |
198 | 361 | self.options.verbosity = 3 | 363 | self.options = options |
199 | 364 | else: | ||
200 | 365 | from pydoctor.driver import parse_args | ||
201 | 366 | self.options, _ = parse_args([]) | ||
202 | 367 | self.options.verbosity = 3 | ||
203 | 368 | |||
204 | 362 | self.abbrevmapping = {} | 369 | self.abbrevmapping = {} |
205 | 363 | self.projectname = 'my project' | 370 | self.projectname = 'my project' |
206 | 364 | self.epytextproblems = [] # fullNames of objects that failed to epytext properly | 371 | self.epytextproblems = [] # fullNames of objects that failed to epytext properly |
207 | @@ -369,6 +376,10 @@ | |||
208 | 369 | self.module_count = 0 | 376 | self.module_count = 0 |
209 | 370 | self.processing_modules = [] | 377 | self.processing_modules = [] |
210 | 371 | self.buildtime = datetime.datetime.now() | 378 | self.buildtime = datetime.datetime.now() |
211 | 379 | # Once pickle support is removed, System should be | ||
212 | 380 | # initialized with project name so that we can reuse intersphinx instace for | ||
213 | 381 | # object.inv generation. | ||
214 | 382 | self.intersphinx = SphinxInventory(logger=self.msg, project_name=self.projectname) | ||
215 | 372 | 383 | ||
216 | 373 | def verbosity(self, section=None): | 384 | def verbosity(self, section=None): |
217 | 374 | if isinstance(section, str): | 385 | if isinstance(section, str): |
218 | @@ -702,3 +713,11 @@ | |||
219 | 702 | while self.unprocessed_modules: | 713 | while self.unprocessed_modules: |
220 | 703 | mod = iter(self.unprocessed_modules).next() | 714 | mod = iter(self.unprocessed_modules).next() |
221 | 704 | self.processModule(mod) | 715 | self.processModule(mod) |
222 | 716 | |||
223 | 717 | |||
224 | 718 | def fetchIntersphinxInventories(self): | ||
225 | 719 | """ | ||
226 | 720 | Download and parse intersphinx inventories based on configuration. | ||
227 | 721 | """ | ||
228 | 722 | for url in self.options.intersphinx: | ||
229 | 723 | self.intersphinx.update(url) | ||
230 | 705 | 724 | ||
231 | === added file 'pydoctor/sphinx.py' | |||
232 | --- pydoctor/sphinx.py 1970-01-01 00:00:00 +0000 | |||
233 | +++ pydoctor/sphinx.py 2014-05-30 08:40:38 +0000 | |||
234 | @@ -0,0 +1,187 @@ | |||
235 | 1 | """ | ||
236 | 2 | Support for Sphinx compatibility. | ||
237 | 3 | """ | ||
238 | 4 | from __future__ import absolute_import | ||
239 | 5 | |||
240 | 6 | import os | ||
241 | 7 | import urllib2 | ||
242 | 8 | import zlib | ||
243 | 9 | |||
244 | 10 | |||
245 | 11 | class SphinxInventory(object): | ||
246 | 12 | """ | ||
247 | 13 | Sphinx inventory handler. | ||
248 | 14 | """ | ||
249 | 15 | |||
250 | 16 | version = (2, 0) | ||
251 | 17 | |||
252 | 18 | def __init__(self, logger, project_name): | ||
253 | 19 | self.project_name = project_name | ||
254 | 20 | self.msg = logger | ||
255 | 21 | self._links = {} | ||
256 | 22 | |||
257 | 23 | def generate(self, subjects, basepath): | ||
258 | 24 | """ | ||
259 | 25 | Generate Sphinx objects inventory version 2 at `basepath`/objects.inv. | ||
260 | 26 | """ | ||
261 | 27 | path = os.path.join(basepath, 'objects.inv') | ||
262 | 28 | self.msg('sphinx', 'Generating objects inventory at %s' % (path,)) | ||
263 | 29 | |||
264 | 30 | with self._openFileForWriting(path) as target: | ||
265 | 31 | target.write(self._generateHeader()) | ||
266 | 32 | content = self._generateContent(subjects) | ||
267 | 33 | target.write(zlib.compress(content)) | ||
268 | 34 | |||
269 | 35 | def _openFileForWriting(self, path): | ||
270 | 36 | """ | ||
271 | 37 | Helper for testing. | ||
272 | 38 | """ | ||
273 | 39 | return open(path, 'w') | ||
274 | 40 | |||
275 | 41 | def _generateHeader(self): | ||
276 | 42 | """ | ||
277 | 43 | Return header for project with name. | ||
278 | 44 | """ | ||
279 | 45 | version = [str(part) for part in self.version] | ||
280 | 46 | return """# Sphinx inventory version 2 | ||
281 | 47 | # Project: %s | ||
282 | 48 | # Version: %s | ||
283 | 49 | # The rest of this file is compressed with zlib. | ||
284 | 50 | """ % (self.project_name, '.'.join(version)) | ||
285 | 51 | |||
286 | 52 | def _generateContent(self, subjects): | ||
287 | 53 | """ | ||
288 | 54 | Write inventory for all `subjects`. | ||
289 | 55 | """ | ||
290 | 56 | content = [] | ||
291 | 57 | for obj in subjects: | ||
292 | 58 | if not obj.isVisible: | ||
293 | 59 | continue | ||
294 | 60 | content.append(self._generateLine(obj)) | ||
295 | 61 | content.append(self._generateContent(obj.orderedcontents)) | ||
296 | 62 | |||
297 | 63 | return ''.join(content) | ||
298 | 64 | |||
299 | 65 | def _generateLine(self, obj): | ||
300 | 66 | """ | ||
301 | 67 | Return inventory line for object. | ||
302 | 68 | |||
303 | 69 | name domain_name:type priority URL display_name | ||
304 | 70 | |||
305 | 71 | Domain name is always: py | ||
306 | 72 | Priority is always: -1 | ||
307 | 73 | Display name is always: - | ||
308 | 74 | """ | ||
309 | 75 | # Avoid circular import. | ||
310 | 76 | from pydoctor import model | ||
311 | 77 | |||
312 | 78 | full_name = obj.fullName() | ||
313 | 79 | |||
314 | 80 | if obj.documentation_location == model.DocLocation.OWN_PAGE: | ||
315 | 81 | url = obj.fullName() + '.html' | ||
316 | 82 | else: | ||
317 | 83 | url = obj.parent.fullName() + '.html#' + obj.name | ||
318 | 84 | |||
319 | 85 | display = '-' | ||
320 | 86 | if isinstance(obj, (model.Package, model.Module)): | ||
321 | 87 | domainname = 'module' | ||
322 | 88 | elif isinstance(obj, model.Class): | ||
323 | 89 | domainname = 'class' | ||
324 | 90 | elif isinstance(obj, model.Function): | ||
325 | 91 | if obj.kind == 'Function': | ||
326 | 92 | domainname = 'function' | ||
327 | 93 | else: | ||
328 | 94 | domainname = 'method' | ||
329 | 95 | elif isinstance(obj, model.Attribute): | ||
330 | 96 | domainname = 'attribute' | ||
331 | 97 | else: | ||
332 | 98 | domainname = 'obj' | ||
333 | 99 | self.msg('sphinx', "Unknown type %r for %s." % (type(obj), full_name,)) | ||
334 | 100 | |||
335 | 101 | return '%s py:%s -1 %s %s\n' % (full_name, domainname, url, display) | ||
336 | 102 | |||
337 | 103 | def update(self, url): | ||
338 | 104 | """ | ||
339 | 105 | Update inventory from URL. | ||
340 | 106 | """ | ||
341 | 107 | parts = url.rsplit('/', 1) | ||
342 | 108 | if len(parts) != 2: | ||
343 | 109 | self.msg( | ||
344 | 110 | 'sphinx', 'Failed to get remote base url for %s' % (url,)) | ||
345 | 111 | return | ||
346 | 112 | |||
347 | 113 | base_url = parts[0] | ||
348 | 114 | |||
349 | 115 | data = self._getURL(url) | ||
350 | 116 | |||
351 | 117 | if not data: | ||
352 | 118 | self.msg( | ||
353 | 119 | 'sphinx', 'Failed to get object inventory from %s' % (url, )) | ||
354 | 120 | return | ||
355 | 121 | |||
356 | 122 | payload = self._getPayload(base_url, data) | ||
357 | 123 | self._links.update(self._parseInventory(base_url, payload)) | ||
358 | 124 | |||
359 | 125 | def _getURL(self, url): | ||
360 | 126 | """ | ||
361 | 127 | Get content of URL. | ||
362 | 128 | |||
363 | 129 | This is a helper for testing. | ||
364 | 130 | """ | ||
365 | 131 | try: | ||
366 | 132 | response = urllib2.urlopen(url) | ||
367 | 133 | return response.read() | ||
368 | 134 | except: | ||
369 | 135 | return None | ||
370 | 136 | |||
371 | 137 | def _getPayload(self, base_url, data): | ||
372 | 138 | """ | ||
373 | 139 | Parse inventory and return clear text payload without comments. | ||
374 | 140 | """ | ||
375 | 141 | payload = '' | ||
376 | 142 | while True: | ||
377 | 143 | parts = data.split('\n', 1) | ||
378 | 144 | if len(parts) != 2: | ||
379 | 145 | payload = data | ||
380 | 146 | break | ||
381 | 147 | if not parts[0].startswith('#'): | ||
382 | 148 | payload = data | ||
383 | 149 | break | ||
384 | 150 | data = parts[1] | ||
385 | 151 | try: | ||
386 | 152 | return zlib.decompress(payload) | ||
387 | 153 | except: | ||
388 | 154 | self.msg( | ||
389 | 155 | 'sphinx', | ||
390 | 156 | 'Failed to uncompress inventory from %s' % (base_url,)) | ||
391 | 157 | return '' | ||
392 | 158 | |||
393 | 159 | def _parseInventory(self, base_url, payload): | ||
394 | 160 | """ | ||
395 | 161 | Parse clear text payload and return a dict with module to link mapping. | ||
396 | 162 | """ | ||
397 | 163 | result = {} | ||
398 | 164 | for line in payload.splitlines(): | ||
399 | 165 | parts = line.split(' ', 4) | ||
400 | 166 | if len(parts) != 5: | ||
401 | 167 | self.msg( | ||
402 | 168 | 'sphinx', | ||
403 | 169 | 'Failed to parse line "%s" for %s' % (line, base_url), | ||
404 | 170 | ) | ||
405 | 171 | continue | ||
406 | 172 | result[parts[0]] = (base_url, parts[3]) | ||
407 | 173 | return result | ||
408 | 174 | |||
409 | 175 | def getLink(self, name): | ||
410 | 176 | """ | ||
411 | 177 | Return link for `name` or None if no link is found. | ||
412 | 178 | """ | ||
413 | 179 | base_url, relative_link = self._links.get(name, (None, None)) | ||
414 | 180 | if not relative_link: | ||
415 | 181 | return None | ||
416 | 182 | |||
417 | 183 | # For links ending with $, replace it with full name. | ||
418 | 184 | if relative_link.endswith('$'): | ||
419 | 185 | relative_link = relative_link[:-1] + name | ||
420 | 186 | |||
421 | 187 | return '%s/%s' % (base_url, relative_link) | ||
422 | 0 | 188 | ||
423 | === renamed file 'pydoctor/sphinx.py' => 'pydoctor/sphinx.py.moved' | |||
424 | === modified file 'pydoctor/test/test_epydoc2stan.py' | |||
425 | --- pydoctor/test/test_epydoc2stan.py 2013-05-23 02:03:27 +0000 | |||
426 | +++ pydoctor/test/test_epydoc2stan.py 2014-05-30 08:40:38 +0000 | |||
427 | @@ -1,6 +1,9 @@ | |||
428 | 1 | from pydoctor import epydoc2stan | 1 | from pydoctor import epydoc2stan |
429 | 2 | from pydoctor import model | ||
430 | 3 | from pydoctor.sphinx import SphinxInventory | ||
431 | 2 | from pydoctor.test.test_astbuilder import fromText | 4 | from pydoctor.test.test_astbuilder import fromText |
432 | 3 | 5 | ||
433 | 6 | |||
434 | 4 | def test_multiple_types(): | 7 | def test_multiple_types(): |
435 | 5 | mod = fromText(''' | 8 | mod = fromText(''' |
436 | 6 | def f(a): | 9 | def f(a): |
437 | @@ -64,3 +67,52 @@ | |||
438 | 64 | assert u'Lorem Ipsum' == get_summary('single_line_summary') | 67 | assert u'Lorem Ipsum' == get_summary('single_line_summary') |
439 | 65 | assert u'Foo Bar Baz' == get_summary('three_lines_summary') | 68 | assert u'Foo Bar Baz' == get_summary('three_lines_summary') |
440 | 66 | assert u'No summary' == get_summary('no_summary') | 69 | assert u'No summary' == get_summary('no_summary') |
441 | 70 | |||
442 | 71 | |||
443 | 72 | def test_EpydocLinker_look_for_intersphinx_no_link(): | ||
444 | 73 | """ | ||
445 | 74 | Return None if inventory had no link for our markup. | ||
446 | 75 | """ | ||
447 | 76 | system = model.System() | ||
448 | 77 | target = model.Module(system, 'ignore-name', 'ignore-docstring') | ||
449 | 78 | sut = epydoc2stan._EpydocLinker(target) | ||
450 | 79 | |||
451 | 80 | result = sut.look_for_intersphinx('base.module') | ||
452 | 81 | |||
453 | 82 | assert None is result | ||
454 | 83 | |||
455 | 84 | |||
456 | 85 | def test_EpydocLinker_look_for_intersphinx_hit(): | ||
457 | 86 | """ | ||
458 | 87 | Return the link from inventory based on first package name. | ||
459 | 88 | """ | ||
460 | 89 | system = model.System() | ||
461 | 90 | inventory = SphinxInventory(system.msg, 'some-project') | ||
462 | 91 | inventory._links['base.module.other'] = ('http://tm.tld', 'some.html') | ||
463 | 92 | system.intersphinx = inventory | ||
464 | 93 | target = model.Module(system, 'ignore-name', 'ignore-docstring') | ||
465 | 94 | sut = epydoc2stan._EpydocLinker(target) | ||
466 | 95 | |||
467 | 96 | result = sut.look_for_intersphinx('base.module.other') | ||
468 | 97 | |||
469 | 98 | assert 'http://tm.tld/some.html' == result | ||
470 | 99 | |||
471 | 100 | |||
472 | 101 | def test_EpydocLinker_translate_identifier_xref_intersphinx(): | ||
473 | 102 | """ | ||
474 | 103 | Return the link from inventory. | ||
475 | 104 | """ | ||
476 | 105 | system = model.System() | ||
477 | 106 | inventory = SphinxInventory(system.msg, 'some-project') | ||
478 | 107 | inventory._links['base.module.other'] = ('http://tm.tld', 'some.html') | ||
479 | 108 | system.intersphinx = inventory | ||
480 | 109 | target = model.Module(system, 'ignore-name', 'ignore-docstring') | ||
481 | 110 | sut = epydoc2stan._EpydocLinker(target) | ||
482 | 111 | |||
483 | 112 | result = sut.translate_identifier_xref( | ||
484 | 113 | 'base.module.other', 'base.module.pretty') | ||
485 | 114 | |||
486 | 115 | expected = ( | ||
487 | 116 | '<a href="http://tm.tld/some.html"><code>base.module.pretty</code></a>' | ||
488 | 117 | ) | ||
489 | 118 | assert expected == result | ||
490 | 67 | 119 | ||
491 | === modified file 'pydoctor/test/test_model.py' | |||
492 | --- pydoctor/test/test_model.py 2008-03-16 19:09:50 +0000 | |||
493 | +++ pydoctor/test/test_model.py 2014-05-30 08:40:38 +0000 | |||
494 | @@ -1,4 +1,11 @@ | |||
495 | 1 | """ | ||
496 | 2 | Unit tests for model. | ||
497 | 3 | """ | ||
498 | 1 | from pydoctor import model | 4 | from pydoctor import model |
499 | 5 | from pydoctor.driver import parse_args | ||
500 | 6 | from pydoctor.sphinx import SphinxInventory | ||
501 | 7 | import zlib | ||
502 | 8 | |||
503 | 2 | 9 | ||
504 | 3 | class FakeOptions(object): | 10 | class FakeOptions(object): |
505 | 4 | """ | 11 | """ |
506 | @@ -41,3 +48,75 @@ | |||
507 | 41 | 48 | ||
508 | 42 | expected = viewSourceBase + moduleRelativePart | 49 | expected = viewSourceBase + moduleRelativePart |
509 | 43 | assert mod.sourceHref == expected | 50 | assert mod.sourceHref == expected |
510 | 51 | |||
511 | 52 | |||
512 | 53 | def test_initialization_default(): | ||
513 | 54 | """ | ||
514 | 55 | When initialized without options, will use default options and default | ||
515 | 56 | verbosity. | ||
516 | 57 | """ | ||
517 | 58 | sut = model.System() | ||
518 | 59 | |||
519 | 60 | assert None is sut.options.projectname | ||
520 | 61 | assert 3 == sut.options.verbosity | ||
521 | 62 | |||
522 | 63 | |||
523 | 64 | def test_initialization_options(): | ||
524 | 65 | """ | ||
525 | 66 | Can be initialized with options. | ||
526 | 67 | """ | ||
527 | 68 | options = object() | ||
528 | 69 | |||
529 | 70 | sut = model.System(options=options) | ||
530 | 71 | |||
531 | 72 | assert options is sut.options | ||
532 | 73 | |||
533 | 74 | |||
534 | 75 | def test_fetchIntersphinxInventories_empty(): | ||
535 | 76 | """ | ||
536 | 77 | Convert option to empty dict. | ||
537 | 78 | """ | ||
538 | 79 | options, _ = parse_args([]) | ||
539 | 80 | options.intersphinx = [] | ||
540 | 81 | sut = model.System(options=options) | ||
541 | 82 | |||
542 | 83 | sut.fetchIntersphinxInventories() | ||
543 | 84 | |||
544 | 85 | # Use internal state since I don't know how else to | ||
545 | 86 | # check for SphinxInventory state. | ||
546 | 87 | assert {} == sut.intersphinx._links | ||
547 | 88 | |||
548 | 89 | |||
549 | 90 | def test_fetchIntersphinxInventories_content(): | ||
550 | 91 | """ | ||
551 | 92 | Download and parse intersphinx inventories for each configured | ||
552 | 93 | intersphix. | ||
553 | 94 | """ | ||
554 | 95 | options, _ = parse_args([]) | ||
555 | 96 | options.intersphinx = [ | ||
556 | 97 | 'http://sphinx/objects.inv', | ||
557 | 98 | 'file:///twisted/index.inv', | ||
558 | 99 | ] | ||
559 | 100 | url_content = { | ||
560 | 101 | 'http://sphinx/objects.inv': zlib.compress( | ||
561 | 102 | 'sphinx.module py:module -1 sp.html -'), | ||
562 | 103 | 'file:///twisted/index.inv': zlib.compress( | ||
563 | 104 | 'twisted.package py:module -1 tm.html -'), | ||
564 | 105 | } | ||
565 | 106 | sut = model.System(options=options) | ||
566 | 107 | log = [] | ||
567 | 108 | sut.msg = lambda part, msg: log.append((part, msg)) | ||
568 | 109 | # Patch url getter to avoid touching the network. | ||
569 | 110 | sut.intersphinx._getURL = lambda url: url_content[url] | ||
570 | 111 | |||
571 | 112 | sut.fetchIntersphinxInventories() | ||
572 | 113 | |||
573 | 114 | assert [] == log | ||
574 | 115 | assert ( | ||
575 | 116 | 'http://sphinx/sp.html' == | ||
576 | 117 | sut.intersphinx.getLink('sphinx.module') | ||
577 | 118 | ) | ||
578 | 119 | assert ( | ||
579 | 120 | 'file:///twisted/tm.html' == | ||
580 | 121 | sut.intersphinx.getLink('twisted.package') | ||
581 | 122 | ) | ||
582 | 44 | 123 | ||
583 | === added file 'pydoctor/test/test_sphinx.py' | |||
584 | --- pydoctor/test/test_sphinx.py 1970-01-01 00:00:00 +0000 | |||
585 | +++ pydoctor/test/test_sphinx.py 2014-05-30 08:40:38 +0000 | |||
586 | @@ -0,0 +1,406 @@ | |||
587 | 1 | """ | ||
588 | 2 | Tests for Sphinx integration. | ||
589 | 3 | """ | ||
590 | 4 | from contextlib import closing | ||
591 | 5 | from StringIO import StringIO | ||
592 | 6 | import zlib | ||
593 | 7 | |||
594 | 8 | from pydoctor import model | ||
595 | 9 | from pydoctor.sphinx import SphinxInventory | ||
596 | 10 | |||
597 | 11 | |||
598 | 12 | class PersistentStringIO(StringIO): | ||
599 | 13 | """ | ||
600 | 14 | A custom stringIO which keeps content after file is closed. | ||
601 | 15 | """ | ||
602 | 16 | def close(self): | ||
603 | 17 | """ | ||
604 | 18 | Close, but keep the memory buffer and seek position. | ||
605 | 19 | """ | ||
606 | 20 | if not self.closed: | ||
607 | 21 | self.closed = True | ||
608 | 22 | |||
609 | 23 | def getvalue(self): | ||
610 | 24 | """ | ||
611 | 25 | Retrieve the entire contents of the "file" at any time even after | ||
612 | 26 | the StringIO object's close() method is called. | ||
613 | 27 | """ | ||
614 | 28 | if self.buflist: | ||
615 | 29 | self.buf += ''.join(self.buflist) | ||
616 | 30 | self.buflist = [] | ||
617 | 31 | return self.buf | ||
618 | 32 | |||
619 | 33 | |||
620 | 34 | def test_initialization(): | ||
621 | 35 | """ | ||
622 | 36 | Is initialized with logger and project name. | ||
623 | 37 | """ | ||
624 | 38 | logger = object() | ||
625 | 39 | name = object() | ||
626 | 40 | |||
627 | 41 | sut = SphinxInventory(logger=logger, project_name=name) | ||
628 | 42 | |||
629 | 43 | assert logger is sut.msg | ||
630 | 44 | assert name is sut.project_name | ||
631 | 45 | |||
632 | 46 | |||
633 | 47 | def test_generate_empty_functional(): | ||
634 | 48 | """ | ||
635 | 49 | Functional test for index generation of empty API. | ||
636 | 50 | |||
637 | 51 | Header is plain text while content is compressed. | ||
638 | 52 | """ | ||
639 | 53 | project_name = 'some-name' | ||
640 | 54 | log = [] | ||
641 | 55 | logger = lambda part, message: log.append((part, message)) | ||
642 | 56 | sut = SphinxInventory(logger=logger, project_name=project_name) | ||
643 | 57 | output = PersistentStringIO() | ||
644 | 58 | sut._openFileForWriting = lambda path: closing(output) | ||
645 | 59 | |||
646 | 60 | sut.generate(subjects=[], basepath='base-path') | ||
647 | 61 | |||
648 | 62 | expected_log = [( | ||
649 | 63 | 'sphinx', | ||
650 | 64 | 'Generating objects inventory at base-path/objects.inv' | ||
651 | 65 | )] | ||
652 | 66 | assert expected_log == log | ||
653 | 67 | |||
654 | 68 | expected_ouput = """# Sphinx inventory version 2 | ||
655 | 69 | # Project: some-name | ||
656 | 70 | # Version: 2.0 | ||
657 | 71 | # The rest of this file is compressed with zlib. | ||
658 | 72 | x\x9c\x03\x00\x00\x00\x00\x01""" | ||
659 | 73 | assert expected_ouput == output.getvalue() | ||
660 | 74 | |||
661 | 75 | |||
662 | 76 | def make_SphinxInventory(): | ||
663 | 77 | """ | ||
664 | 78 | Return a SphinxInventory. | ||
665 | 79 | """ | ||
666 | 80 | return SphinxInventory(logger=object(), project_name='project_name') | ||
667 | 81 | |||
668 | 82 | |||
669 | 83 | def test_generateContent(): | ||
670 | 84 | """ | ||
671 | 85 | Return a string with inventory for all targeted objects, recursive. | ||
672 | 86 | """ | ||
673 | 87 | sut = make_SphinxInventory() | ||
674 | 88 | system = model.System() | ||
675 | 89 | root1 = model.Package(system, 'package1', 'docstring1') | ||
676 | 90 | root2 = model.Package(system, 'package2', 'docstring2') | ||
677 | 91 | child1 = model.Package(system, 'child1', 'docstring3', parent=root2) | ||
678 | 92 | system.addObject(child1) | ||
679 | 93 | subjects = [root1, root2] | ||
680 | 94 | |||
681 | 95 | result = sut._generateContent(subjects) | ||
682 | 96 | |||
683 | 97 | expected_result = ( | ||
684 | 98 | 'package1 py:module -1 package1.html -\n' | ||
685 | 99 | 'package2 py:module -1 package2.html -\n' | ||
686 | 100 | 'package2.child1 py:module -1 package2.child1.html -\n' | ||
687 | 101 | ) | ||
688 | 102 | assert expected_result == result | ||
689 | 103 | |||
690 | 104 | |||
691 | 105 | def test_generateLine_package(): | ||
692 | 106 | """ | ||
693 | 107 | Check inventory for package. | ||
694 | 108 | """ | ||
695 | 109 | sut = make_SphinxInventory() | ||
696 | 110 | |||
697 | 111 | result = sut._generateLine( | ||
698 | 112 | model.Package('ignore-system', 'package1', 'ignore-docstring')) | ||
699 | 113 | |||
700 | 114 | assert 'package1 py:module -1 package1.html -\n' == result | ||
701 | 115 | |||
702 | 116 | |||
703 | 117 | def test_generateLine_module(): | ||
704 | 118 | """ | ||
705 | 119 | Check inventory for module. | ||
706 | 120 | """ | ||
707 | 121 | sut = make_SphinxInventory() | ||
708 | 122 | |||
709 | 123 | result = sut._generateLine( | ||
710 | 124 | model.Module('ignore-system', 'module1', 'ignore-docstring')) | ||
711 | 125 | |||
712 | 126 | assert 'module1 py:module -1 module1.html -\n' == result | ||
713 | 127 | |||
714 | 128 | |||
715 | 129 | def test_generateLine_class(): | ||
716 | 130 | """ | ||
717 | 131 | Check inventory for class. | ||
718 | 132 | """ | ||
719 | 133 | sut = make_SphinxInventory() | ||
720 | 134 | |||
721 | 135 | result = sut._generateLine( | ||
722 | 136 | model.Class('ignore-system', 'class1', 'ignore-docstring')) | ||
723 | 137 | |||
724 | 138 | assert 'class1 py:class -1 class1.html -\n' == result | ||
725 | 139 | |||
726 | 140 | |||
727 | 141 | def test_generateLine_function(): | ||
728 | 142 | """ | ||
729 | 143 | Check inventory for function. | ||
730 | 144 | |||
731 | 145 | Functions are inside a module. | ||
732 | 146 | """ | ||
733 | 147 | sut = make_SphinxInventory() | ||
734 | 148 | parent = model.Module('ignore-system', 'module1', 'docstring') | ||
735 | 149 | |||
736 | 150 | result = sut._generateLine( | ||
737 | 151 | model.Function('ignore-system', 'func1', 'ignore-docstring', parent)) | ||
738 | 152 | |||
739 | 153 | assert 'module1.func1 py:function -1 module1.html#func1 -\n' == result | ||
740 | 154 | |||
741 | 155 | |||
742 | 156 | def test_generateLine_method(): | ||
743 | 157 | """ | ||
744 | 158 | Check inventory for method. | ||
745 | 159 | |||
746 | 160 | Methods are functions inside a class. | ||
747 | 161 | """ | ||
748 | 162 | sut = make_SphinxInventory() | ||
749 | 163 | parent = model.Class('ignore-system', 'class1', 'docstring') | ||
750 | 164 | |||
751 | 165 | result = sut._generateLine( | ||
752 | 166 | model.Function('ignore-system', 'meth1', 'ignore-docstring', parent)) | ||
753 | 167 | |||
754 | 168 | assert 'class1.meth1 py:method -1 class1.html#meth1 -\n' == result | ||
755 | 169 | |||
756 | 170 | |||
757 | 171 | def test_generateLine_attribute(): | ||
758 | 172 | """ | ||
759 | 173 | Check inventory for attributes. | ||
760 | 174 | """ | ||
761 | 175 | sut = make_SphinxInventory() | ||
762 | 176 | parent = model.Class('ignore-system', 'class1', 'docstring') | ||
763 | 177 | |||
764 | 178 | result = sut._generateLine( | ||
765 | 179 | model.Attribute('ignore-system', 'attr1', 'ignore-docstring', parent)) | ||
766 | 180 | |||
767 | 181 | assert 'class1.attr1 py:attribute -1 class1.html#attr1 -\n' == result | ||
768 | 182 | |||
769 | 183 | |||
770 | 184 | class UnknownType(model.Documentable): | ||
771 | 185 | """ | ||
772 | 186 | Documentable type to help with testing. | ||
773 | 187 | """ | ||
774 | 188 | |||
775 | 189 | |||
776 | 190 | def test_generateLine_unknown(): | ||
777 | 191 | """ | ||
778 | 192 | When object type is uknown a message is logged and is handled as | ||
779 | 193 | generic object. | ||
780 | 194 | """ | ||
781 | 195 | log = [] | ||
782 | 196 | sut = make_SphinxInventory() | ||
783 | 197 | sut.msg = lambda part, message: log.append((part, message)) | ||
784 | 198 | |||
785 | 199 | result = sut._generateLine( | ||
786 | 200 | UnknownType('ignore-system', 'unknown1', 'ignore-docstring')) | ||
787 | 201 | |||
788 | 202 | assert 'unknown1 py:obj -1 unknown1.html -\n' == result | ||
789 | 203 | |||
790 | 204 | |||
791 | 205 | def make_SphinxInventory(): | ||
792 | 206 | """ | ||
793 | 207 | Return a SphinxInventory. | ||
794 | 208 | """ | ||
795 | 209 | return SphinxInventory(logger=object(), project_name='project_name') | ||
796 | 210 | |||
797 | 211 | |||
798 | 212 | def make_SphinxInventoryWithLog(): | ||
799 | 213 | """ | ||
800 | 214 | Return a SphinxInventory with patched log. | ||
801 | 215 | """ | ||
802 | 216 | inventory = make_SphinxInventory() | ||
803 | 217 | log = [] | ||
804 | 218 | inventory.msg = lambda part, msg: log.append((part, msg)) | ||
805 | 219 | return (inventory, log) | ||
806 | 220 | |||
807 | 221 | |||
808 | 222 | def test_getPayload_empty(): | ||
809 | 223 | """ | ||
810 | 224 | Return empty string. | ||
811 | 225 | """ | ||
812 | 226 | sut = make_SphinxInventory() | ||
813 | 227 | content = """# Sphinx inventory version 2 | ||
814 | 228 | # Project: some-name | ||
815 | 229 | # Version: 2.0 | ||
816 | 230 | # The rest of this file is compressed with zlib. | ||
817 | 231 | x\x9c\x03\x00\x00\x00\x00\x01""" | ||
818 | 232 | |||
819 | 233 | result = sut._getPayload('http://base.ignore', content) | ||
820 | 234 | |||
821 | 235 | assert '' == result | ||
822 | 236 | |||
823 | 237 | |||
824 | 238 | def test_getPayload_content(): | ||
825 | 239 | """ | ||
826 | 240 | Return content as string. | ||
827 | 241 | """ | ||
828 | 242 | payload = 'first_line\nsecond line' | ||
829 | 243 | sut = make_SphinxInventory() | ||
830 | 244 | content = """# Ignored line | ||
831 | 245 | # Project: some-name | ||
832 | 246 | # Version: 2.0 | ||
833 | 247 | # commented line. | ||
834 | 248 | %s""" % (zlib.compress(payload),) | ||
835 | 249 | |||
836 | 250 | result = sut._getPayload('http://base.ignore', content) | ||
837 | 251 | |||
838 | 252 | assert payload == result | ||
839 | 253 | |||
840 | 254 | |||
841 | 255 | def test_getPayload_invalid(): | ||
842 | 256 | """ | ||
843 | 257 | Return empty string and log an error when failing to uncompress data. | ||
844 | 258 | """ | ||
845 | 259 | sut, log = make_SphinxInventoryWithLog() | ||
846 | 260 | base_url = 'http://tm.tld' | ||
847 | 261 | content = """# Project: some-name | ||
848 | 262 | # Version: 2.0 | ||
849 | 263 | not-valid-zlib-content""" | ||
850 | 264 | |||
851 | 265 | result = sut._getPayload(base_url, content) | ||
852 | 266 | |||
853 | 267 | assert '' == result | ||
854 | 268 | assert [( | ||
855 | 269 | 'sphinx', 'Failed to uncompress inventory from http://tm.tld', | ||
856 | 270 | )] == log | ||
857 | 271 | |||
858 | 272 | |||
859 | 273 | def test_getLink_not_found(): | ||
860 | 274 | """ | ||
861 | 275 | Return None if link does not exists. | ||
862 | 276 | """ | ||
863 | 277 | sut = make_SphinxInventory() | ||
864 | 278 | |||
865 | 279 | assert None is sut.getLink('no.such.name') | ||
866 | 280 | |||
867 | 281 | |||
868 | 282 | def test_getLink_found(): | ||
869 | 283 | """ | ||
870 | 284 | Return the link from internal state. | ||
871 | 285 | """ | ||
872 | 286 | sut = make_SphinxInventory() | ||
873 | 287 | sut._links['some.name'] = ('http://base.tld', 'some/url.php') | ||
874 | 288 | |||
875 | 289 | assert 'http://base.tld/some/url.php' == sut.getLink('some.name') | ||
876 | 290 | |||
877 | 291 | |||
878 | 292 | def test_getLink_self_anchor(): | ||
879 | 293 | """ | ||
880 | 294 | Return the link with anchor as target name when link end with $. | ||
881 | 295 | """ | ||
882 | 296 | sut = make_SphinxInventory() | ||
883 | 297 | sut._links['some.name'] = ('http://base.tld', 'some/url.php#$') | ||
884 | 298 | |||
885 | 299 | assert 'http://base.tld/some/url.php#some.name' == sut.getLink('some.name') | ||
886 | 300 | |||
887 | 301 | |||
888 | 302 | def test_update_functional(): | ||
889 | 303 | """ | ||
890 | 304 | Functional test for updating from an empty inventory. | ||
891 | 305 | """ | ||
892 | 306 | payload = ( | ||
893 | 307 | 'some.module1 py:module -1 module1.html -\n' | ||
894 | 308 | 'other.module2 py:module 0 module2.html Other description\n' | ||
895 | 309 | ) | ||
896 | 310 | sut = make_SphinxInventory() | ||
897 | 311 | # Patch URL loader to avoid hitting the system. | ||
898 | 312 | content = """# Sphinx inventory version 2 | ||
899 | 313 | # Project: some-name | ||
900 | 314 | # Version: 2.0 | ||
901 | 315 | # The rest of this file is compressed with zlib. | ||
902 | 316 | %s""" % (zlib.compress(payload),) | ||
903 | 317 | sut._getURL = lambda _: content | ||
904 | 318 | |||
905 | 319 | sut.update('http://some.url/api/objects.inv') | ||
906 | 320 | |||
907 | 321 | assert 'http://some.url/api/module1.html' == sut.getLink('some.module1') | ||
908 | 322 | assert 'http://some.url/api/module2.html' == sut.getLink('other.module2') | ||
909 | 323 | |||
910 | 324 | |||
911 | 325 | def test_update_bad_url(): | ||
912 | 326 | """ | ||
913 | 327 | Log an error when failing to get base url from url. | ||
914 | 328 | """ | ||
915 | 329 | sut, log = make_SphinxInventoryWithLog() | ||
916 | 330 | |||
917 | 331 | sut.update('really.bad.url') | ||
918 | 332 | |||
919 | 333 | assert sut._links == {} | ||
920 | 334 | expected_log = [( | ||
921 | 335 | 'sphinx', 'Failed to get remote base url for really.bad.url' | ||
922 | 336 | )] | ||
923 | 337 | assert expected_log == log | ||
924 | 338 | |||
925 | 339 | |||
926 | 340 | def test_update_fail(): | ||
927 | 341 | """ | ||
928 | 342 | Log an error when failing to get content from url. | ||
929 | 343 | """ | ||
930 | 344 | sut, log = make_SphinxInventoryWithLog() | ||
931 | 345 | sut._getURL = lambda _: None | ||
932 | 346 | |||
933 | 347 | sut.update('http://some.tld/o.inv') | ||
934 | 348 | |||
935 | 349 | assert sut._links == {} | ||
936 | 350 | expected_log = [( | ||
937 | 351 | 'sphinx', 'Failed to get object inventory from http://some.tld/o.inv' | ||
938 | 352 | )] | ||
939 | 353 | assert expected_log == log | ||
940 | 354 | |||
941 | 355 | |||
942 | 356 | def test_parseInventory_empty(): | ||
943 | 357 | """ | ||
944 | 358 | Return empty dict for empty input. | ||
945 | 359 | """ | ||
946 | 360 | sut = make_SphinxInventory() | ||
947 | 361 | |||
948 | 362 | result = sut._parseInventory('http://base.tld', '') | ||
949 | 363 | |||
950 | 364 | assert {} == result | ||
951 | 365 | |||
952 | 366 | |||
953 | 367 | def test_parseInventory_single_line(): | ||
954 | 368 | """ | ||
955 | 369 | Return a dict with a single member. | ||
956 | 370 | """ | ||
957 | 371 | sut = make_SphinxInventory() | ||
958 | 372 | |||
959 | 373 | result = sut._parseInventory( | ||
960 | 374 | 'http://base.tld', 'some.attr py:attr -1 some.html De scription') | ||
961 | 375 | |||
962 | 376 | assert {'some.attr': ('http://base.tld', 'some.html')} == result | ||
963 | 377 | |||
964 | 378 | |||
965 | 379 | def test_parseInventory_invalid_lines(): | ||
966 | 380 | """ | ||
967 | 381 | Skip line and log an error. | ||
968 | 382 | """ | ||
969 | 383 | sut, log = make_SphinxInventoryWithLog() | ||
970 | 384 | base_url = 'http://tm.tld' | ||
971 | 385 | content = ( | ||
972 | 386 | 'good.attr py:attribute -1 some.html -\n' | ||
973 | 387 | 'bad.attr bad format\n' | ||
974 | 388 | 'very.bad\n' | ||
975 | 389 | '\n' | ||
976 | 390 | 'good.again py:module 0 again.html -\n' | ||
977 | 391 | ) | ||
978 | 392 | |||
979 | 393 | result = sut._parseInventory(base_url, content) | ||
980 | 394 | |||
981 | 395 | assert { | ||
982 | 396 | 'good.attr': (base_url, 'some.html'), | ||
983 | 397 | 'good.again': (base_url, 'again.html'), | ||
984 | 398 | } == result | ||
985 | 399 | assert [ | ||
986 | 400 | ( | ||
987 | 401 | 'sphinx', | ||
988 | 402 | 'Failed to parse line "bad.attr bad format" for http://tm.tld' | ||
989 | 403 | ), | ||
990 | 404 | ('sphinx', 'Failed to parse line "very.bad" for http://tm.tld'), | ||
991 | 405 | ('sphinx', 'Failed to parse line "" for http://tm.tld'), | ||
992 | 406 | ] == log | ||
993 | 0 | 407 | ||
994 | === renamed file 'pydoctor/test/test_sphinx.py' => 'pydoctor/test/test_sphinx.py.moved' |
Adi Roiban <email address hidden> writes:
> Adi Roiban has proposed merging lp:~adiroiban/pydoctor/1318325-intersphinx into lp:pydoctor. /bugs.launchpad .net/pydoctor/ +bug/1318325 /code.launchpad .net/~adiroiban /pydoctor/ 1318325- intersphinx/ +merge/ 219128
>
> Requested reviews:
> Michael Hudson-Doyle (mwhudson)
> Related bugs:
> Bug #1318325 in pydoctor: "Support linking to interlinks objects.inv"
> https:/
>
> For more details, see:
> https:/
>
> Problem description
> -------------------
>
> pydoctor has an hardcoded links for stdlibs but it would be nice to be able
> to link to sphinx apidocs using Sphinx inventory files
Heh, more amazingness!
> domain. tld/latest/ objects. inv will using 'http:// domain. tld/latest/ objects. inv' for any link starting with 'sphinx'
> Changes
> -------
>
> Added '--intersphinx' configuration option.
>
> It assumes that a remote object inventory contains indexed for a single root package.
> This is why sphinx:http://
>
> System was updated to allow initialization with options and to download and parse requested objects inventory.
>
> Intersphinx inventories are stored using a simple dict and they provide a getLink method.
>
> epydoc2stan linker was updated to look for intersphinx links when previous lookups fail.
I guess in the fullness of time it would be nice to:
a) support caching the objects.inv so that doc generation doesn't have
to download it each time.
b) allow the project being documented to define which objects.inv to use
neither of these feel at all urgent though.
> How to test addnodes. desc_optional> } addnodes. desc_optional} sphinx:http:// sphinx. readthedocs. org/en/ latest/ objects. inv pydoctor /code.launchpad .net/~adiroiban /pydoctor/ 1318325- intersphinx/ +merge/ 219128
> -----------
>
> Check that README documentation make sense... not sure where to put this
> info.
>
> I have also done a simple manual test using sphinx object.inv from rtd.org
>
> Set one of the following markups
>
> def getparser():
> """
> Return parser for command line options.
>
> L{pretty <sphinx.
> """
>
> or
>
> def getparser():
> """
> Return parser for command line options.
>
> L{sphinx.
> """
>
> build pydoctor using
>
> pydoctor --intersphinx=
>
> I tried to write as much test as possible but this is a weekend hack and there might be gaps in coverage.. or errors in code.
>
> Please check that changes make sense.
> Let me know what needs to be changes or if you have better idea for how
> this could be implemented.
>
> I don't have to much experience with pydoctor usage and how people expect
> to link to 3rd party.
>
> Thanks!
>
> --
> https:/
> You are requested to review the proposed merge of lp:~adiroiban/pydoctor/1318325-intersphinx into lp:pydoctor.
> === modified file 'README.txt'
> --- README.txt 2013-05-29 03:28:59 +0000
> +++ README.txt 2014-05-12 01:08:05 +0000
> @@ -14,3 +14,20 @@
> The default HTML generator requires Twisted.
>
> There are some more notes in the doc/ subdirectory.
> +
> +
> +Sphinx Integration
> +------------------
> +
> +It can link to external API documentation using Sphinx objects inventory using
> +the following cumulative configuration option::
> +
> + --in...