Merge lp:~adiroiban/pydoctor/1287458-objects.inv into lp:~mwhudson/pydoctor/dev

Proposed by Adi Roiban
Status: Merged
Merged at revision: 615
Proposed branch: lp:~adiroiban/pydoctor/1287458-objects.inv
Merge into: lp:~mwhudson/pydoctor/dev
Diff against target: 386 lines (+344/-0)
4 files modified
README.txt (+30/-0)
pydoctor/driver.py (+15/-0)
pydoctor/sphinx.py (+98/-0)
pydoctor/test/test_sphinx.py (+201/-0)
To merge this branch: bzr merge lp:~adiroiban/pydoctor/1287458-objects.inv
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Pending
Review via email: mp+219116@code.launchpad.net

Description of the change

Project
------

pydoctor pickle system is broken.

Rather than fixing picker, the solution was to support sphinx inventory objects.inv

Changes
-------

I have note removed pickle code, but created a separate ticket for that.. as I have no idea how pickle should be used.

When html is generated it will alwasy generate an objects.inv file.
I did not add any options, trying to keep things simple.

Also file is hardcoded to objects.inv and there is no option to use a different file name.

I have added some tests and updated documentation.

How to test
-----------

I tried this on my local computer with sphinx documentation. Syntax is documented in README.

intersphinx plugin can be configured with local paths:

intersphinx_mapping = {
    'py2': ('http://docs.python.org/2.7', None),
    'py3': ('http://docs.python.org/3.3', None),
    'pydoctor': ('file:///home/adi/chevah/pydoctor/apidocs', None),
}

Please check that changes make sense and let me know if something needs changes.
Thanks!

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

Woah! This is awesome, thanks for working on this.

Just a few comments, but they're pretty minor. I'd be happy to merge
this as is, but I'll give you time to reply first.

Adi Roiban <email address hidden> writes:

> Adi Roiban has proposed merging lp:~adiroiban/pydoctor/1287458-objects.inv into lp:pydoctor.
>
> Requested reviews:
> Michael Hudson-Doyle (mwhudson)
> Related bugs:
> Bug #1287458 in pydoctor: "Generate Sphinx object inventory instead of pickle"
> https://bugs.launchpad.net/pydoctor/+bug/1287458
>
> For more details, see:
> https://code.launchpad.net/~adiroiban/pydoctor/1287458-objects.inv/+merge/219116
>
> Project
> ------
>
> pydoctor pickle system is broken.
>
> Rather than fixing picker, the solution was to support sphinx inventory objects.inv
>
>
> Changes
> -------
>
> I have note removed pickle code, but created a separate ticket for that.. as I have no idea how pickle should be used.

pickle should not be used, ever. I think I even knew this when I was
first writing pydoctor all those years ago, I don't know what I was
thinking.

> When html is generated it will alwasy generate an objects.inv file.
> I did not add any options, trying to keep things simple.
>
> Also file is hardcoded to objects.inv and there is no option to use a different file name.

That's also the way sphinx works right? It assumes that there is a
objects.inv at $base_url/objects.inv? If so, I agree there is no point
adding flexibility to pydoctor...

> I have added some tests and updated documentation.

Thanks :-)

> How to test
> -----------
>
> I tried this on my local computer with sphinx documentation. Syntax is documented in README.
>
> intersphinx plugin can be configured with local paths:
>
> intersphinx_mapping = {
> 'py2': ('http://docs.python.org/2.7', None),
> 'py3': ('http://docs.python.org/3.3', None),
> 'pydoctor': ('file:///home/adi/chevah/pydoctor/apidocs', None),
> }
>
> Please check that changes make sense and let me know if something needs changes.
> Thanks!
>
> --
> https://code.launchpad.net/~adiroiban/pydoctor/1287458-objects.inv/+merge/219116
> You are requested to review the proposed merge of lp:~adiroiban/pydoctor/1287458-objects.inv into lp:pydoctor.
> === modified file 'README.txt'
> --- README.txt 2013-05-29 03:28:59 +0000
> +++ README.txt 2014-05-11 11:23:52 +0000
> @@ -14,3 +14,33 @@
> The default HTML generator requires Twisted.
>
> There are some more notes in the doc/ subdirectory.
> +
> +
> +Sphinx Integration
> +------------------
> +
> +HTML generator will also generate a Sphinx objects inventory using the
> +following mapping:
> +
> +* packages, modules -> py:mod:
> +* classes -> py:class:
> +* functions -> py:func:
> +* methods -> py:meth:
> +* attributes -> py:attr:
> +
> +Configure Sphinx intersphinx extension:
> +
> + intersphinx_mapping = {
> + 'pydoctor': ('http://domain.tld/api', None),
> + }
> +
> +Use external references::
> +
> + :py:func:`External API <pydoctor:pydoctor.model.Documentable.reparent>`
> +
> + :py:mod:`pydoctor:pydoctor`
> + :py:mod:`pydoctor:pydoctor.model`
> + :py:func:`pydoctor:pydoctor.driver.getparser`
> + :py:class:`pydoctor:pydoctor.m...

616. By Adi Roiban <email address hidden>

Update after review.

Revision history for this message
Adi Roiban (adiroiban) wrote :

>> Also file is hardcoded to objects.inv and there is no option to use a different file name.
>
> That's also the way sphinx works right? It assumes that there is a
> objects.inv at $base_url/objects.inv? If so, I agree there is no point
> adding flexibility to pydoctor...

This is the default... not sure why Sphinx configuration allows a custom file name.. maybe for extra flexibility.
I think that we can start with simple default and this can be later extended.

>> def error(msg, *args):
>> @@ -331,6 +332,8 @@
>> 'warning',
>> 'WARNING: guessing '+name+' for project name', thresh=-1)
>> system.guessedprojectname = name
>> + else:
>> + system.guessedprojectname = system.options.projectname
>
> So this seems a bit strange ... I guess there should be a
> system.projectname that gets set here and that should be referred to in
> the places that currently check options.projectname and fall back to
> guessedprojectname. I can do this if you don't want to though :-)

OK. I have refactored this to use system.projectname.

I am not sure why we still have projectname and guessedprojectname.

Please check that I have not done anything stupid.

>> + domainname = 'obj'
>> + base_url = full_name
>> + self.msg('sphinx', "Unknown type for %s." % (full_name,))
>
> This seems like it would be cleaner to do a mapping based on obj.kind I
> think?
>
Done!

Thanks for the review!

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

Adi Roiban <email address hidden> writes:

>>> Also file is hardcoded to objects.inv and there is no option to use a different file name.
>>
>> That's also the way sphinx works right? It assumes that there is a
>> objects.inv at $base_url/objects.inv? If so, I agree there is no point
>> adding flexibility to pydoctor...
>
> This is the default... not sure why Sphinx configuration allows a custom file name.. maybe for extra flexibility.
> I think that we can start with simple default and this can be later extended.

+1

>
>>> def error(msg, *args):
>>> @@ -331,6 +332,8 @@
>>> 'warning',
>>> 'WARNING: guessing '+name+' for project name', thresh=-1)
>>> system.guessedprojectname = name
>>> + else:
>>> + system.guessedprojectname = system.options.projectname
>>
>> So this seems a bit strange ... I guess there should be a
>> system.projectname that gets set here and that should be referred to in
>> the places that currently check options.projectname and fall back to
>> guessedprojectname. I can do this if you don't want to though :-)
>
> OK. I have refactored this to use system.projectname.
>
> I am not sure why we still have projectname and guessedprojectname.

Thanks.

> Please check that I have not done anything stupid.

It looks OK.

>>> + domainname = 'obj'
>>> + base_url = full_name
>>> + self.msg('sphinx', "Unknown type for %s." % (full_name,))
>>
>> This seems like it would be cleaner to do a mapping based on obj.kind I
>> think?
>>
> Done!

Argh, so this was probably a bad suggestion, because .kind is actually
quite varied -- it can get set to, for example, "Instance Variable" or
"Class Method". So what had before was probably better.

But, to rescue myself a bit, you only need to do this to determine the
"domain". I think you can construct the link part by examining
obj.documentation_location -- see taglink in templatewriter/utils.py
(which could/should probably have the link computation part extracted --
I don't know why the 'link' function in there is like that, it's
misleadingly named :(). This also makes me realize that objects.inv
generation should take obj.isVisible into account...

Sorry for not mentioning this stuff earlier -- do you want to make these
changes? I can probably do it too, now the brain cells are warmed up a
bit.

Cheers,
mwh

Revision history for this message
Adi Roiban (adiroiban) wrote :

Regarding obj,kind obj.documentation_location, if you have time, please do the refactor.

If you don't have time, I can also do this... but I will be busy until 1 of June.

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 2013-05-29 03:28:59 +0000
3+++ README.txt 2014-05-12 01:26:49 +0000
4@@ -14,3 +14,33 @@
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
39=== modified file 'pydoctor/driver.py'
40--- pydoctor/driver.py 2013-05-29 22:51:44 +0000
41+++ pydoctor/driver.py 2014-05-12 01:26:49 +0000
42@@ -1,6 +1,7 @@
43 """The command-line parsing and entry point."""
44
45 from pydoctor import model, zopeinterface
46+from pydoctor.sphinx import SphinxInventory
47 import sys, os
48
49 def error(msg, *args):
50@@ -331,6 +332,9 @@
51 'warning',
52 'WARNING: guessing '+name+' for project name', thresh=-1)
53 system.guessedprojectname = name
54+ system.projectname = name
55+ else:
56+ system.projectname = system.options.projectname
57
58 # step 4: save the system, if desired
59
60@@ -391,6 +395,17 @@
61 f.close()
62 system.options = options
63
64+ # Generate Sphinx inventory.
65+ sphinx_inventory = SphinxInventory(
66+ logger=system.msg,
67+ project_name=system.projectname,
68+ )
69+ sphinx_inventory.generate(
70+ subjects=subjects,
71+ basepath=options.htmloutput,
72+ )
73+
74+
75 # Finally, if we should serve html, lets serve some html.
76 if options.server:
77 from pydoctor.server import (
78
79=== added file 'pydoctor/sphinx.py'
80--- pydoctor/sphinx.py 1970-01-01 00:00:00 +0000
81+++ pydoctor/sphinx.py 2014-05-12 01:26:49 +0000
82@@ -0,0 +1,98 @@
83+"""
84+Support for Sphinx compatibility.
85+"""
86+from __future__ import absolute_import
87+import os
88+import zlib
89+
90+
91+class SphinxInventory(object):
92+ """
93+ Sphinx inventory handler.
94+ """
95+
96+ version = (2, 0)
97+
98+ def __init__(self, logger, project_name):
99+ self.project_name = project_name
100+ self.msg = logger
101+
102+ def generate(self, subjects, basepath):
103+ """
104+ Generate Sphinx objects inventory version 2 at `basepath`/objects.inv.
105+ """
106+ path = os.path.join(basepath, 'objects.inv')
107+ self.msg('sphinx', 'Generating objects inventory at %s' % (path,))
108+
109+ with self._openFileForWriting(path) as target:
110+ target.write(self._generateHeader())
111+ content = self._generateContent(subjects)
112+ target.write(zlib.compress(content))
113+
114+ def _openFileForWriting(self, path):
115+ """
116+ Helper for testing.
117+ """
118+ return open(path, 'w')
119+
120+ def _generateHeader(self):
121+ """
122+ Return header for project with name.
123+ """
124+ version = [str(part) for part in self.version]
125+ return """# Sphinx inventory version 2
126+# Project: %s
127+# Version: %s
128+# The rest of this file is compressed with zlib.
129+""" % (self.project_name, '.'.join(version))
130+
131+ def _generateContent(self, subjects):
132+ """
133+ Write inventory for all `subjects`.
134+ """
135+ content = []
136+ for obj in subjects:
137+ content.append(self._generateLine(obj))
138+ content.append(self._generateContent(obj.orderedcontents))
139+
140+ return ''.join(content)
141+
142+ def _generateLine(self, obj):
143+ """
144+ Return inventory line for object.
145+
146+ name domain_name:type priority URL display_name
147+
148+ Domain name is always: py
149+ Priority is always: -1
150+ Display name is always: -
151+ """
152+ full_name = obj.fullName()
153+ full_parent_name = ''
154+ if obj.parent:
155+ full_parent_name = obj.parent.fullName()
156+ display = '-'
157+ mapping = {
158+ 'package': ('module', full_name, None),
159+ 'module': ('module', full_name, None),
160+ 'class': ('class', full_name, None),
161+ 'method': ('method', full_parent_name, obj.name),
162+ 'function': ('function', full_parent_name, obj.name),
163+ 'attribute': ('attribute', full_parent_name, obj.name),
164+ }
165+
166+ try:
167+ domainname, base_url, anchor = mapping[obj.kind.lower()]
168+ except (KeyError, AttributeError):
169+ domainname = 'obj'
170+ base_url = full_name
171+ anchor = None
172+ self.msg('sphinx', "Unknown type for %s." % (full_name,))
173+
174+ base_url += '.html'
175+ if anchor:
176+ url = '%s#%s' % (base_url, anchor)
177+ else:
178+ url = base_url
179+
180+ return '%s py:%s -1 %s %s\n' % (full_name, domainname, url, display)
181
182=== added file 'pydoctor/test/test_sphinx.py'
183--- pydoctor/test/test_sphinx.py 1970-01-01 00:00:00 +0000
184+++ pydoctor/test/test_sphinx.py 2014-05-12 01:26:49 +0000
185@@ -0,0 +1,201 @@
186+"""
187+Tests for Sphinx integration.
188+"""
189+from contextlib import closing
190+from StringIO import StringIO
191+
192+from pydoctor import model
193+from pydoctor.sphinx import SphinxInventory
194+
195+
196+class PersistentStringIO(StringIO):
197+ """
198+ A custom stringIO which keeps content after file is closed.
199+ """
200+ def close(self):
201+ """
202+ Close, but keep the memory buffer and seek position.
203+ """
204+ if not self.closed:
205+ self.closed = True
206+
207+ def getvalue(self):
208+ """
209+ Retrieve the entire contents of the "file" at any time even after
210+ the StringIO object's close() method is called.
211+ """
212+ if self.buflist:
213+ self.buf += ''.join(self.buflist)
214+ self.buflist = []
215+ return self.buf
216+
217+
218+def test_initialization():
219+ """
220+ Is initialized with logger and project name.
221+ """
222+ logger = object()
223+ name = object()
224+
225+ sut = SphinxInventory(logger=logger, project_name=name)
226+
227+ assert logger is sut.msg
228+ assert name is sut.project_name
229+
230+
231+def test_generate_empty_functional():
232+ """
233+ Functional test for index generation of empty API.
234+
235+ Header is plain text while content is compressed.
236+ """
237+ project_name = 'some-name'
238+ log = []
239+ logger = lambda part, message: log.append((part, message))
240+ sut = SphinxInventory(logger=logger, project_name=project_name)
241+ output = PersistentStringIO()
242+ sut._openFileForWriting = lambda path: closing(output)
243+
244+ sut.generate(subjects=[], basepath='base-path')
245+
246+ expected_log = [(
247+ 'sphinx',
248+ 'Generating objects inventory at base-path/objects.inv'
249+ )]
250+ assert expected_log == log
251+
252+ expected_ouput = """# Sphinx inventory version 2
253+# Project: some-name
254+# Version: 2.0
255+# The rest of this file is compressed with zlib.
256+x\x9c\x03\x00\x00\x00\x00\x01"""
257+ assert expected_ouput == output.getvalue()
258+
259+
260+def make_SphinxInventory():
261+ """
262+ Return a SphinxInventory.
263+ """
264+ return SphinxInventory(logger=object(), project_name='project_name')
265+
266+
267+def test_generateContent():
268+ """
269+ Return a string with inventory for all targeted objects, recursive.
270+ """
271+ sut = make_SphinxInventory()
272+ system = model.System()
273+ root1 = model.Package(system, 'package1', 'docstring1')
274+ root2 = model.Package(system, 'package2', 'docstring2')
275+ child1 = model.Package(system, 'child1', 'docstring3', parent=root2)
276+ system.addObject(child1)
277+ subjects = [root1, root2]
278+
279+ result = sut._generateContent(subjects)
280+
281+ expected_result = (
282+ 'package1 py:module -1 package1.html -\n'
283+ 'package2 py:module -1 package2.html -\n'
284+ 'package2.child1 py:module -1 package2.child1.html -\n'
285+ )
286+ assert expected_result == result
287+
288+
289+def test_generateLine_package():
290+ """
291+ Check inventory for package.
292+ """
293+ sut = make_SphinxInventory()
294+
295+ result = sut._generateLine(
296+ model.Package('ignore-system', 'package1', 'ignore-docstring'))
297+
298+ assert 'package1 py:module -1 package1.html -\n' == result
299+
300+
301+def test_generateLine_module():
302+ """
303+ Check inventory for module.
304+ """
305+ sut = make_SphinxInventory()
306+
307+ result = sut._generateLine(
308+ model.Module('ignore-system', 'module1', 'ignore-docstring'))
309+
310+ assert 'module1 py:module -1 module1.html -\n' == result
311+
312+
313+def test_generateLine_class():
314+ """
315+ Check inventory for class.
316+ """
317+ sut = make_SphinxInventory()
318+
319+ result = sut._generateLine(
320+ model.Class('ignore-system', 'class1', 'ignore-docstring'))
321+
322+ assert 'class1 py:class -1 class1.html -\n' == result
323+
324+
325+def test_generateLine_function():
326+ """
327+ Check inventory for function.
328+
329+ Functions are inside a module.
330+ """
331+ sut = make_SphinxInventory()
332+ parent = model.Module('ignore-system', 'module1', 'docstring')
333+
334+ result = sut._generateLine(
335+ model.Function('ignore-system', 'func1', 'ignore-docstring', parent))
336+
337+ assert 'module1.func1 py:function -1 module1.html#func1 -\n' == result
338+
339+
340+def test_generateLine_method():
341+ """
342+ Check inventory for method.
343+
344+ Methods are functions inside a class.
345+ """
346+ sut = make_SphinxInventory()
347+ parent = model.Class('ignore-system', 'class1', 'docstring')
348+
349+ result = sut._generateLine(
350+ model.Function('ignore-system', 'meth1', 'ignore-docstring', parent))
351+
352+ assert 'class1.meth1 py:method -1 class1.html#meth1 -\n' == result
353+
354+
355+def test_generateLine_attribute():
356+ """
357+ Check inventory for attributes.
358+ """
359+ sut = make_SphinxInventory()
360+ parent = model.Class('ignore-system', 'class1', 'docstring')
361+
362+ result = sut._generateLine(
363+ model.Attribute('ignore-system', 'attr1', 'ignore-docstring', parent))
364+
365+ assert 'class1.attr1 py:attribute -1 class1.html#attr1 -\n' == result
366+
367+
368+class UnknownType(model.Documentable):
369+ """
370+ Documentable type to help with testing.
371+ """
372+
373+
374+def test_generateLine_unknown():
375+ """
376+ When object type is uknown a message is logged and is handled as
377+ generic object.
378+ """
379+ log = []
380+ sut = make_SphinxInventory()
381+ sut.msg = lambda part, message: log.append((part, message))
382+
383+ result = sut._generateLine(
384+ UnknownType('ignore-system', 'unknown1', 'ignore-docstring'))
385+
386+ assert 'unknown1 py:obj -1 unknown1.html -\n' == result

Subscribers

People subscribed via source and target branches

to all changes:
to status/vote changes: