Merge lp:~adiroiban/pydoctor/1318325-intersphinx into lp:~mwhudson/pydoctor/dev

Proposed by Adi Roiban
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
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Pending
Review via email: mp+219128@code.launchpad.net

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://domain.tld/latest/objects.inv will using 'http://domain.tld/latest/objects.inv' for any link starting with 'sphinx'

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.addnodes.desc_optional>}
    """

or

def getparser():
    """
    Return parser for command line options.

    L{sphinx.addnodes.desc_optional}
    """

build pydoctor using

pydoctor --intersphinx=sphinx:http://sphinx.readthedocs.org/en/latest/objects.inv pydoctor

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!

To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :
Download full text (23.5 KiB)

Adi Roiban <email address hidden> writes:

> Adi Roiban has proposed merging lp:~adiroiban/pydoctor/1318325-intersphinx into lp:pydoctor.
>
> Requested reviews:
> Michael Hudson-Doyle (mwhudson)
> Related bugs:
> Bug #1318325 in pydoctor: "Support linking to interlinks objects.inv"
> https://bugs.launchpad.net/pydoctor/+bug/1318325
>
> For more details, see:
> https://code.launchpad.net/~adiroiban/pydoctor/1318325-intersphinx/+merge/219128
>
> 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!

>
> Changes
> -------
>
> Added '--intersphinx' configuration option.
>
> It assumes that a remote object inventory contains indexed for a single root package.
> This is why sphinx:http://domain.tld/latest/objects.inv will using 'http://domain.tld/latest/objects.inv' for any link starting with 'sphinx'
>
> 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
> -----------
>
> 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.addnodes.desc_optional>}
> """
>
> or
>
> def getparser():
> """
> Return parser for command line options.
>
> L{sphinx.addnodes.desc_optional}
> """
>
> build pydoctor using
>
> pydoctor --intersphinx=sphinx:http://sphinx.readthedocs.org/en/latest/objects.inv pydoctor
>
> 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://code.launchpad.net/~adiroiban/pydoctor/1318325-intersphinx/+merge/219128
> 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...

Revision history for this message
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!

Revision history for this message
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!

Revision history for this message
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...

Revision history for this message
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://docs.python.org/2.7', None),
 'py3': ('http://docs.python.org/3.2', None),
}

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://url/to/objects.inv should be enough.

---

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!

Revision history for this message
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://docs.python.org/2.7', None),
> 'py3': ('http://docs.python.org/3.2', None),
> }
>
> 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://url/to/objects.inv should be enough.

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

616. By Adi Roiban <email address hidden>

Merge master.

617. By Adi Roiban <email address hidden>

Update with a single db for all intersphinx.

Revision history for this message
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.

Revision history for this message
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.

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Well fiiiiiiiiiiiiiinally I sorted out the conflicts and merged this. Sorry for letting it sit for so very long.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 The default HTML generator requires Twisted.
6
7 There are some more notes in the doc/ subdirectory.
8-
9-
10-Sphinx Integration
11-------------------
12-
13-HTML generator will also generate a Sphinx objects inventory using the
14-following mapping:
15-
16-* packages, modules -> py:mod:
17-* classes -> py:class:
18-* functions -> py:func:
19-* methods -> py:meth:
20-* attributes -> py:attr:
21-
22-Configure Sphinx intersphinx extension:
23-
24- intersphinx_mapping = {
25- 'pydoctor': ('http://domain.tld/api', None),
26- }
27-
28-Use external references::
29-
30- :py:func:`External API <pydoctor:pydoctor.model.Documentable.reparent>`
31-
32- :py:mod:`pydoctor:pydoctor`
33- :py:mod:`pydoctor:pydoctor.model`
34- :py:func:`pydoctor:pydoctor.driver.getparser`
35- :py:class:`pydoctor:pydoctor.model.Documentable`
36- :py:meth:`pydoctor:pydoctor.model.Documentable.reparent`
37- :py:attr:`pydoctor:pydoctor.model.Documentable.kind`
38+<<<<<<< TREE
39+
40+
41+Sphinx Integration
42+------------------
43+
44+HTML generator will also generate a Sphinx objects inventory using the
45+following mapping:
46+
47+* packages, modules -> py:mod:
48+* classes -> py:class:
49+* functions -> py:func:
50+* methods -> py:meth:
51+* attributes -> py:attr:
52+
53+Configure Sphinx intersphinx extension:
54+
55+ intersphinx_mapping = {
56+ 'pydoctor': ('http://domain.tld/api', None),
57+ }
58+
59+Use external references::
60+
61+ :py:func:`External API <pydoctor:pydoctor.model.Documentable.reparent>`
62+
63+ :py:mod:`pydoctor:pydoctor`
64+ :py:mod:`pydoctor:pydoctor.model`
65+ :py:func:`pydoctor:pydoctor.driver.getparser`
66+ :py:class:`pydoctor:pydoctor.model.Documentable`
67+ :py:meth:`pydoctor:pydoctor.model.Documentable.reparent`
68+ :py:attr:`pydoctor:pydoctor.model.Documentable.kind`
69+=======
70+
71+
72+Sphinx Integration
73+------------------
74+
75+HTML generator will also generate a Sphinx objects inventory using the
76+following mapping:
77+
78+* packages, modules -> py:mod:
79+* classes -> py:class:
80+* functions -> py:func:
81+* methods -> py:meth:
82+* attributes -> py:attr:
83+
84+Configure Sphinx intersphinx extension:
85+
86+ intersphinx_mapping = {
87+ 'pydoctor': ('http://domain.tld/api', None),
88+ }
89+
90+Use external references::
91+
92+ :py:func:`External API <pydoctor:pydoctor.model.Documentable.reparent>`
93+
94+ :py:mod:`pydoctor:pydoctor`
95+ :py:mod:`pydoctor:pydoctor.model`
96+ :py:func:`pydoctor:pydoctor.driver.getparser`
97+ :py:class:`pydoctor:pydoctor.model.Documentable`
98+ :py:meth:`pydoctor:pydoctor.model.Documentable.reparent`
99+ :py:attr:`pydoctor:pydoctor.model.Documentable.kind`
100+
101+It can link to external API documentation using Sphinx objects inventory using
102+the following cumulative configuration option::
103+
104+ --intersphinx=http://sphinx.org/objects.inv
105+>>>>>>> MERGE-SOURCE
106
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 parser.add_option(
112 '--introspect-c-modules', default=False, action='store_true',
113 help=("Import and introspect any C modules found."))
114+
115+ parser.add_option(
116+ '--intersphinx', action='append', dest='intersphinx',
117+ metavar='URL_TO_OBJECTS.INV', default=[],
118+ help=(
119+ "Use Sphinx objects inventory to generate links to external"
120+ "documetation. Can be repeated."))
121+
122 return parser
123
124 def readConfigFile(options):
125@@ -252,7 +260,10 @@
126 else:
127 system = systemclass()
128
129+ # Once pickle support is removed, always instantiate System with
130+ # options and make fetchIntersphinxInventories private in __init__.
131 system.options = options
132+ system.fetchIntersphinxInventories()
133
134 system.urlprefix = ''
135 if options.moresystems:
136
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 thresh=-1)
142 return None
143
144+ def look_for_intersphinx(self, name):
145+ """
146+ Return link for `name` based on intersphinx inventory.
147+
148+ Return None if link is not found.
149+ """
150+ return self.obj.system.intersphinx.getLink(name)
151+
152 def translate_identifier_xref(self, fullID, prettyID):
153 """Figure out what ``L{fullID}`` should link to.
154
155@@ -163,6 +171,11 @@
156 self.obj.system.objectsOfType(model.Package)))
157 if target is not None:
158 return self._objLink(target, prettyID)
159+
160+ target = self.look_for_intersphinx(fullID)
161+ if target:
162+ return '<a href="%s"><code>%s</code></a>'%(target, prettyID)
163+
164 self.obj.system.msg(
165 "translate_identifier_xref", "%s:%s invalid ref to %s" % (
166 self.obj.fullName(), self.obj.linenumber, fullID),
167
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 import types
173 import __builtin__
174
175+from pydoctor.sphinx import SphinxInventory
176+
177 # originally when I started to write pydoctor I had this idea of a big
178 # tree of Documentables arranged in an almost arbitrary tree.
179 #
180@@ -347,7 +349,7 @@
181 #defaultBuilder = astbuilder.ASTBuilder
182 sourcebase = None
183
184- def __init__(self):
185+ def __init__(self, options=None):
186 self.allobjects = {}
187 self.orderedallobjects = []
188 self.rootobjects = []
189@@ -356,9 +358,14 @@
190 self.moresystems = []
191 self.subsystems = []
192 self.urlprefix = ''
193- from pydoctor.driver import parse_args
194- self.options, _ = parse_args([])
195- self.options.verbosity = 3
196+
197+ if options:
198+ self.options = options
199+ else:
200+ from pydoctor.driver import parse_args
201+ self.options, _ = parse_args([])
202+ self.options.verbosity = 3
203+
204 self.abbrevmapping = {}
205 self.projectname = 'my project'
206 self.epytextproblems = [] # fullNames of objects that failed to epytext properly
207@@ -369,6 +376,10 @@
208 self.module_count = 0
209 self.processing_modules = []
210 self.buildtime = datetime.datetime.now()
211+ # Once pickle support is removed, System should be
212+ # initialized with project name so that we can reuse intersphinx instace for
213+ # object.inv generation.
214+ self.intersphinx = SphinxInventory(logger=self.msg, project_name=self.projectname)
215
216 def verbosity(self, section=None):
217 if isinstance(section, str):
218@@ -702,3 +713,11 @@
219 while self.unprocessed_modules:
220 mod = iter(self.unprocessed_modules).next()
221 self.processModule(mod)
222+
223+
224+ def fetchIntersphinxInventories(self):
225+ """
226+ Download and parse intersphinx inventories based on configuration.
227+ """
228+ for url in self.options.intersphinx:
229+ self.intersphinx.update(url)
230
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+"""
236+Support for Sphinx compatibility.
237+"""
238+from __future__ import absolute_import
239+
240+import os
241+import urllib2
242+import zlib
243+
244+
245+class SphinxInventory(object):
246+ """
247+ Sphinx inventory handler.
248+ """
249+
250+ version = (2, 0)
251+
252+ def __init__(self, logger, project_name):
253+ self.project_name = project_name
254+ self.msg = logger
255+ self._links = {}
256+
257+ def generate(self, subjects, basepath):
258+ """
259+ Generate Sphinx objects inventory version 2 at `basepath`/objects.inv.
260+ """
261+ path = os.path.join(basepath, 'objects.inv')
262+ self.msg('sphinx', 'Generating objects inventory at %s' % (path,))
263+
264+ with self._openFileForWriting(path) as target:
265+ target.write(self._generateHeader())
266+ content = self._generateContent(subjects)
267+ target.write(zlib.compress(content))
268+
269+ def _openFileForWriting(self, path):
270+ """
271+ Helper for testing.
272+ """
273+ return open(path, 'w')
274+
275+ def _generateHeader(self):
276+ """
277+ Return header for project with name.
278+ """
279+ version = [str(part) for part in self.version]
280+ return """# Sphinx inventory version 2
281+# Project: %s
282+# Version: %s
283+# The rest of this file is compressed with zlib.
284+""" % (self.project_name, '.'.join(version))
285+
286+ def _generateContent(self, subjects):
287+ """
288+ Write inventory for all `subjects`.
289+ """
290+ content = []
291+ for obj in subjects:
292+ if not obj.isVisible:
293+ continue
294+ content.append(self._generateLine(obj))
295+ content.append(self._generateContent(obj.orderedcontents))
296+
297+ return ''.join(content)
298+
299+ def _generateLine(self, obj):
300+ """
301+ Return inventory line for object.
302+
303+ name domain_name:type priority URL display_name
304+
305+ Domain name is always: py
306+ Priority is always: -1
307+ Display name is always: -
308+ """
309+ # Avoid circular import.
310+ from pydoctor import model
311+
312+ full_name = obj.fullName()
313+
314+ if obj.documentation_location == model.DocLocation.OWN_PAGE:
315+ url = obj.fullName() + '.html'
316+ else:
317+ url = obj.parent.fullName() + '.html#' + obj.name
318+
319+ display = '-'
320+ if isinstance(obj, (model.Package, model.Module)):
321+ domainname = 'module'
322+ elif isinstance(obj, model.Class):
323+ domainname = 'class'
324+ elif isinstance(obj, model.Function):
325+ if obj.kind == 'Function':
326+ domainname = 'function'
327+ else:
328+ domainname = 'method'
329+ elif isinstance(obj, model.Attribute):
330+ domainname = 'attribute'
331+ else:
332+ domainname = 'obj'
333+ self.msg('sphinx', "Unknown type %r for %s." % (type(obj), full_name,))
334+
335+ return '%s py:%s -1 %s %s\n' % (full_name, domainname, url, display)
336+
337+ def update(self, url):
338+ """
339+ Update inventory from URL.
340+ """
341+ parts = url.rsplit('/', 1)
342+ if len(parts) != 2:
343+ self.msg(
344+ 'sphinx', 'Failed to get remote base url for %s' % (url,))
345+ return
346+
347+ base_url = parts[0]
348+
349+ data = self._getURL(url)
350+
351+ if not data:
352+ self.msg(
353+ 'sphinx', 'Failed to get object inventory from %s' % (url, ))
354+ return
355+
356+ payload = self._getPayload(base_url, data)
357+ self._links.update(self._parseInventory(base_url, payload))
358+
359+ def _getURL(self, url):
360+ """
361+ Get content of URL.
362+
363+ This is a helper for testing.
364+ """
365+ try:
366+ response = urllib2.urlopen(url)
367+ return response.read()
368+ except:
369+ return None
370+
371+ def _getPayload(self, base_url, data):
372+ """
373+ Parse inventory and return clear text payload without comments.
374+ """
375+ payload = ''
376+ while True:
377+ parts = data.split('\n', 1)
378+ if len(parts) != 2:
379+ payload = data
380+ break
381+ if not parts[0].startswith('#'):
382+ payload = data
383+ break
384+ data = parts[1]
385+ try:
386+ return zlib.decompress(payload)
387+ except:
388+ self.msg(
389+ 'sphinx',
390+ 'Failed to uncompress inventory from %s' % (base_url,))
391+ return ''
392+
393+ def _parseInventory(self, base_url, payload):
394+ """
395+ Parse clear text payload and return a dict with module to link mapping.
396+ """
397+ result = {}
398+ for line in payload.splitlines():
399+ parts = line.split(' ', 4)
400+ if len(parts) != 5:
401+ self.msg(
402+ 'sphinx',
403+ 'Failed to parse line "%s" for %s' % (line, base_url),
404+ )
405+ continue
406+ result[parts[0]] = (base_url, parts[3])
407+ return result
408+
409+ def getLink(self, name):
410+ """
411+ Return link for `name` or None if no link is found.
412+ """
413+ base_url, relative_link = self._links.get(name, (None, None))
414+ if not relative_link:
415+ return None
416+
417+ # For links ending with $, replace it with full name.
418+ if relative_link.endswith('$'):
419+ relative_link = relative_link[:-1] + name
420+
421+ return '%s/%s' % (base_url, relative_link)
422
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 from pydoctor import epydoc2stan
429+from pydoctor import model
430+from pydoctor.sphinx import SphinxInventory
431 from pydoctor.test.test_astbuilder import fromText
432
433+
434 def test_multiple_types():
435 mod = fromText('''
436 def f(a):
437@@ -64,3 +67,52 @@
438 assert u'Lorem Ipsum' == get_summary('single_line_summary')
439 assert u'Foo Bar Baz' == get_summary('three_lines_summary')
440 assert u'No summary' == get_summary('no_summary')
441+
442+
443+def test_EpydocLinker_look_for_intersphinx_no_link():
444+ """
445+ Return None if inventory had no link for our markup.
446+ """
447+ system = model.System()
448+ target = model.Module(system, 'ignore-name', 'ignore-docstring')
449+ sut = epydoc2stan._EpydocLinker(target)
450+
451+ result = sut.look_for_intersphinx('base.module')
452+
453+ assert None is result
454+
455+
456+def test_EpydocLinker_look_for_intersphinx_hit():
457+ """
458+ Return the link from inventory based on first package name.
459+ """
460+ system = model.System()
461+ inventory = SphinxInventory(system.msg, 'some-project')
462+ inventory._links['base.module.other'] = ('http://tm.tld', 'some.html')
463+ system.intersphinx = inventory
464+ target = model.Module(system, 'ignore-name', 'ignore-docstring')
465+ sut = epydoc2stan._EpydocLinker(target)
466+
467+ result = sut.look_for_intersphinx('base.module.other')
468+
469+ assert 'http://tm.tld/some.html' == result
470+
471+
472+def test_EpydocLinker_translate_identifier_xref_intersphinx():
473+ """
474+ Return the link from inventory.
475+ """
476+ system = model.System()
477+ inventory = SphinxInventory(system.msg, 'some-project')
478+ inventory._links['base.module.other'] = ('http://tm.tld', 'some.html')
479+ system.intersphinx = inventory
480+ target = model.Module(system, 'ignore-name', 'ignore-docstring')
481+ sut = epydoc2stan._EpydocLinker(target)
482+
483+ result = sut.translate_identifier_xref(
484+ 'base.module.other', 'base.module.pretty')
485+
486+ expected = (
487+ '<a href="http://tm.tld/some.html"><code>base.module.pretty</code></a>'
488+ )
489+ assert expected == result
490
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+"""
496+Unit tests for model.
497+"""
498 from pydoctor import model
499+from pydoctor.driver import parse_args
500+from pydoctor.sphinx import SphinxInventory
501+import zlib
502+
503
504 class FakeOptions(object):
505 """
506@@ -41,3 +48,75 @@
507
508 expected = viewSourceBase + moduleRelativePart
509 assert mod.sourceHref == expected
510+
511+
512+def test_initialization_default():
513+ """
514+ When initialized without options, will use default options and default
515+ verbosity.
516+ """
517+ sut = model.System()
518+
519+ assert None is sut.options.projectname
520+ assert 3 == sut.options.verbosity
521+
522+
523+def test_initialization_options():
524+ """
525+ Can be initialized with options.
526+ """
527+ options = object()
528+
529+ sut = model.System(options=options)
530+
531+ assert options is sut.options
532+
533+
534+def test_fetchIntersphinxInventories_empty():
535+ """
536+ Convert option to empty dict.
537+ """
538+ options, _ = parse_args([])
539+ options.intersphinx = []
540+ sut = model.System(options=options)
541+
542+ sut.fetchIntersphinxInventories()
543+
544+ # Use internal state since I don't know how else to
545+ # check for SphinxInventory state.
546+ assert {} == sut.intersphinx._links
547+
548+
549+def test_fetchIntersphinxInventories_content():
550+ """
551+ Download and parse intersphinx inventories for each configured
552+ intersphix.
553+ """
554+ options, _ = parse_args([])
555+ options.intersphinx = [
556+ 'http://sphinx/objects.inv',
557+ 'file:///twisted/index.inv',
558+ ]
559+ url_content = {
560+ 'http://sphinx/objects.inv': zlib.compress(
561+ 'sphinx.module py:module -1 sp.html -'),
562+ 'file:///twisted/index.inv': zlib.compress(
563+ 'twisted.package py:module -1 tm.html -'),
564+ }
565+ sut = model.System(options=options)
566+ log = []
567+ sut.msg = lambda part, msg: log.append((part, msg))
568+ # Patch url getter to avoid touching the network.
569+ sut.intersphinx._getURL = lambda url: url_content[url]
570+
571+ sut.fetchIntersphinxInventories()
572+
573+ assert [] == log
574+ assert (
575+ 'http://sphinx/sp.html' ==
576+ sut.intersphinx.getLink('sphinx.module')
577+ )
578+ assert (
579+ 'file:///twisted/tm.html' ==
580+ sut.intersphinx.getLink('twisted.package')
581+ )
582
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+"""
588+Tests for Sphinx integration.
589+"""
590+from contextlib import closing
591+from StringIO import StringIO
592+import zlib
593+
594+from pydoctor import model
595+from pydoctor.sphinx import SphinxInventory
596+
597+
598+class PersistentStringIO(StringIO):
599+ """
600+ A custom stringIO which keeps content after file is closed.
601+ """
602+ def close(self):
603+ """
604+ Close, but keep the memory buffer and seek position.
605+ """
606+ if not self.closed:
607+ self.closed = True
608+
609+ def getvalue(self):
610+ """
611+ Retrieve the entire contents of the "file" at any time even after
612+ the StringIO object's close() method is called.
613+ """
614+ if self.buflist:
615+ self.buf += ''.join(self.buflist)
616+ self.buflist = []
617+ return self.buf
618+
619+
620+def test_initialization():
621+ """
622+ Is initialized with logger and project name.
623+ """
624+ logger = object()
625+ name = object()
626+
627+ sut = SphinxInventory(logger=logger, project_name=name)
628+
629+ assert logger is sut.msg
630+ assert name is sut.project_name
631+
632+
633+def test_generate_empty_functional():
634+ """
635+ Functional test for index generation of empty API.
636+
637+ Header is plain text while content is compressed.
638+ """
639+ project_name = 'some-name'
640+ log = []
641+ logger = lambda part, message: log.append((part, message))
642+ sut = SphinxInventory(logger=logger, project_name=project_name)
643+ output = PersistentStringIO()
644+ sut._openFileForWriting = lambda path: closing(output)
645+
646+ sut.generate(subjects=[], basepath='base-path')
647+
648+ expected_log = [(
649+ 'sphinx',
650+ 'Generating objects inventory at base-path/objects.inv'
651+ )]
652+ assert expected_log == log
653+
654+ expected_ouput = """# Sphinx inventory version 2
655+# Project: some-name
656+# Version: 2.0
657+# The rest of this file is compressed with zlib.
658+x\x9c\x03\x00\x00\x00\x00\x01"""
659+ assert expected_ouput == output.getvalue()
660+
661+
662+def make_SphinxInventory():
663+ """
664+ Return a SphinxInventory.
665+ """
666+ return SphinxInventory(logger=object(), project_name='project_name')
667+
668+
669+def test_generateContent():
670+ """
671+ Return a string with inventory for all targeted objects, recursive.
672+ """
673+ sut = make_SphinxInventory()
674+ system = model.System()
675+ root1 = model.Package(system, 'package1', 'docstring1')
676+ root2 = model.Package(system, 'package2', 'docstring2')
677+ child1 = model.Package(system, 'child1', 'docstring3', parent=root2)
678+ system.addObject(child1)
679+ subjects = [root1, root2]
680+
681+ result = sut._generateContent(subjects)
682+
683+ expected_result = (
684+ 'package1 py:module -1 package1.html -\n'
685+ 'package2 py:module -1 package2.html -\n'
686+ 'package2.child1 py:module -1 package2.child1.html -\n'
687+ )
688+ assert expected_result == result
689+
690+
691+def test_generateLine_package():
692+ """
693+ Check inventory for package.
694+ """
695+ sut = make_SphinxInventory()
696+
697+ result = sut._generateLine(
698+ model.Package('ignore-system', 'package1', 'ignore-docstring'))
699+
700+ assert 'package1 py:module -1 package1.html -\n' == result
701+
702+
703+def test_generateLine_module():
704+ """
705+ Check inventory for module.
706+ """
707+ sut = make_SphinxInventory()
708+
709+ result = sut._generateLine(
710+ model.Module('ignore-system', 'module1', 'ignore-docstring'))
711+
712+ assert 'module1 py:module -1 module1.html -\n' == result
713+
714+
715+def test_generateLine_class():
716+ """
717+ Check inventory for class.
718+ """
719+ sut = make_SphinxInventory()
720+
721+ result = sut._generateLine(
722+ model.Class('ignore-system', 'class1', 'ignore-docstring'))
723+
724+ assert 'class1 py:class -1 class1.html -\n' == result
725+
726+
727+def test_generateLine_function():
728+ """
729+ Check inventory for function.
730+
731+ Functions are inside a module.
732+ """
733+ sut = make_SphinxInventory()
734+ parent = model.Module('ignore-system', 'module1', 'docstring')
735+
736+ result = sut._generateLine(
737+ model.Function('ignore-system', 'func1', 'ignore-docstring', parent))
738+
739+ assert 'module1.func1 py:function -1 module1.html#func1 -\n' == result
740+
741+
742+def test_generateLine_method():
743+ """
744+ Check inventory for method.
745+
746+ Methods are functions inside a class.
747+ """
748+ sut = make_SphinxInventory()
749+ parent = model.Class('ignore-system', 'class1', 'docstring')
750+
751+ result = sut._generateLine(
752+ model.Function('ignore-system', 'meth1', 'ignore-docstring', parent))
753+
754+ assert 'class1.meth1 py:method -1 class1.html#meth1 -\n' == result
755+
756+
757+def test_generateLine_attribute():
758+ """
759+ Check inventory for attributes.
760+ """
761+ sut = make_SphinxInventory()
762+ parent = model.Class('ignore-system', 'class1', 'docstring')
763+
764+ result = sut._generateLine(
765+ model.Attribute('ignore-system', 'attr1', 'ignore-docstring', parent))
766+
767+ assert 'class1.attr1 py:attribute -1 class1.html#attr1 -\n' == result
768+
769+
770+class UnknownType(model.Documentable):
771+ """
772+ Documentable type to help with testing.
773+ """
774+
775+
776+def test_generateLine_unknown():
777+ """
778+ When object type is uknown a message is logged and is handled as
779+ generic object.
780+ """
781+ log = []
782+ sut = make_SphinxInventory()
783+ sut.msg = lambda part, message: log.append((part, message))
784+
785+ result = sut._generateLine(
786+ UnknownType('ignore-system', 'unknown1', 'ignore-docstring'))
787+
788+ assert 'unknown1 py:obj -1 unknown1.html -\n' == result
789+
790+
791+def make_SphinxInventory():
792+ """
793+ Return a SphinxInventory.
794+ """
795+ return SphinxInventory(logger=object(), project_name='project_name')
796+
797+
798+def make_SphinxInventoryWithLog():
799+ """
800+ Return a SphinxInventory with patched log.
801+ """
802+ inventory = make_SphinxInventory()
803+ log = []
804+ inventory.msg = lambda part, msg: log.append((part, msg))
805+ return (inventory, log)
806+
807+
808+def test_getPayload_empty():
809+ """
810+ Return empty string.
811+ """
812+ sut = make_SphinxInventory()
813+ content = """# Sphinx inventory version 2
814+# Project: some-name
815+# Version: 2.0
816+# The rest of this file is compressed with zlib.
817+x\x9c\x03\x00\x00\x00\x00\x01"""
818+
819+ result = sut._getPayload('http://base.ignore', content)
820+
821+ assert '' == result
822+
823+
824+def test_getPayload_content():
825+ """
826+ Return content as string.
827+ """
828+ payload = 'first_line\nsecond line'
829+ sut = make_SphinxInventory()
830+ content = """# Ignored line
831+# Project: some-name
832+# Version: 2.0
833+# commented line.
834+%s""" % (zlib.compress(payload),)
835+
836+ result = sut._getPayload('http://base.ignore', content)
837+
838+ assert payload == result
839+
840+
841+def test_getPayload_invalid():
842+ """
843+ Return empty string and log an error when failing to uncompress data.
844+ """
845+ sut, log = make_SphinxInventoryWithLog()
846+ base_url = 'http://tm.tld'
847+ content = """# Project: some-name
848+# Version: 2.0
849+not-valid-zlib-content"""
850+
851+ result = sut._getPayload(base_url, content)
852+
853+ assert '' == result
854+ assert [(
855+ 'sphinx', 'Failed to uncompress inventory from http://tm.tld',
856+ )] == log
857+
858+
859+def test_getLink_not_found():
860+ """
861+ Return None if link does not exists.
862+ """
863+ sut = make_SphinxInventory()
864+
865+ assert None is sut.getLink('no.such.name')
866+
867+
868+def test_getLink_found():
869+ """
870+ Return the link from internal state.
871+ """
872+ sut = make_SphinxInventory()
873+ sut._links['some.name'] = ('http://base.tld', 'some/url.php')
874+
875+ assert 'http://base.tld/some/url.php' == sut.getLink('some.name')
876+
877+
878+def test_getLink_self_anchor():
879+ """
880+ Return the link with anchor as target name when link end with $.
881+ """
882+ sut = make_SphinxInventory()
883+ sut._links['some.name'] = ('http://base.tld', 'some/url.php#$')
884+
885+ assert 'http://base.tld/some/url.php#some.name' == sut.getLink('some.name')
886+
887+
888+def test_update_functional():
889+ """
890+ Functional test for updating from an empty inventory.
891+ """
892+ payload = (
893+ 'some.module1 py:module -1 module1.html -\n'
894+ 'other.module2 py:module 0 module2.html Other description\n'
895+ )
896+ sut = make_SphinxInventory()
897+ # Patch URL loader to avoid hitting the system.
898+ content = """# Sphinx inventory version 2
899+# Project: some-name
900+# Version: 2.0
901+# The rest of this file is compressed with zlib.
902+%s""" % (zlib.compress(payload),)
903+ sut._getURL = lambda _: content
904+
905+ sut.update('http://some.url/api/objects.inv')
906+
907+ assert 'http://some.url/api/module1.html' == sut.getLink('some.module1')
908+ assert 'http://some.url/api/module2.html' == sut.getLink('other.module2')
909+
910+
911+def test_update_bad_url():
912+ """
913+ Log an error when failing to get base url from url.
914+ """
915+ sut, log = make_SphinxInventoryWithLog()
916+
917+ sut.update('really.bad.url')
918+
919+ assert sut._links == {}
920+ expected_log = [(
921+ 'sphinx', 'Failed to get remote base url for really.bad.url'
922+ )]
923+ assert expected_log == log
924+
925+
926+def test_update_fail():
927+ """
928+ Log an error when failing to get content from url.
929+ """
930+ sut, log = make_SphinxInventoryWithLog()
931+ sut._getURL = lambda _: None
932+
933+ sut.update('http://some.tld/o.inv')
934+
935+ assert sut._links == {}
936+ expected_log = [(
937+ 'sphinx', 'Failed to get object inventory from http://some.tld/o.inv'
938+ )]
939+ assert expected_log == log
940+
941+
942+def test_parseInventory_empty():
943+ """
944+ Return empty dict for empty input.
945+ """
946+ sut = make_SphinxInventory()
947+
948+ result = sut._parseInventory('http://base.tld', '')
949+
950+ assert {} == result
951+
952+
953+def test_parseInventory_single_line():
954+ """
955+ Return a dict with a single member.
956+ """
957+ sut = make_SphinxInventory()
958+
959+ result = sut._parseInventory(
960+ 'http://base.tld', 'some.attr py:attr -1 some.html De scription')
961+
962+ assert {'some.attr': ('http://base.tld', 'some.html')} == result
963+
964+
965+def test_parseInventory_invalid_lines():
966+ """
967+ Skip line and log an error.
968+ """
969+ sut, log = make_SphinxInventoryWithLog()
970+ base_url = 'http://tm.tld'
971+ content = (
972+ 'good.attr py:attribute -1 some.html -\n'
973+ 'bad.attr bad format\n'
974+ 'very.bad\n'
975+ '\n'
976+ 'good.again py:module 0 again.html -\n'
977+ )
978+
979+ result = sut._parseInventory(base_url, content)
980+
981+ assert {
982+ 'good.attr': (base_url, 'some.html'),
983+ 'good.again': (base_url, 'again.html'),
984+ } == result
985+ assert [
986+ (
987+ 'sphinx',
988+ 'Failed to parse line "bad.attr bad format" for http://tm.tld'
989+ ),
990+ ('sphinx', 'Failed to parse line "very.bad" for http://tm.tld'),
991+ ('sphinx', 'Failed to parse line "" for http://tm.tld'),
992+ ] == log
993
994=== renamed file 'pydoctor/test/test_sphinx.py' => 'pydoctor/test/test_sphinx.py.moved'

Subscribers

People subscribed via source and target branches

to all changes:
to status/vote changes: