sagemath / sage_sample

A sample project illustrating how to write and distribute SageMath extensions
GNU General Public License v2.0
17 stars 15 forks source link

Add support for readthedocs #6

Open nthiery opened 7 years ago

nthiery commented 7 years ago

https://readthedocs.org/ can be a nice way for sage packages to make their documentation available. So it would be nice to investigate how to showcase how to do this in sage_sample.

A first attempt is here: https://readthedocs.org/projects/sage-sample/

As expected, the difficulty is that compiling the documentation with Sphinx requires importing the Sage library. So we need to figure out a way to specify that Sage is a dependency (see readthedocs's build process). At this stage, we can't do it in setup.py.

There also are some ressource limitations; but as long as we do a binary install of Sage, that should not be a problem (unless they have also a disk space limitation).

cc: @embray

embray commented 7 years ago

With some cajoling, and with proper support from our end, RTD might be willing to make some Sage version available on their build machines. This is still non-trivial as a project's documentation might build correctly against one Sage version but not another and Sage has no reliable API (a serious problem, but one beyond the scope of this).

In the past I believe they've been willing to add non-trivial binary dependencies on their build system if there was enough demand. And if we are willing to help support that effort (as opposed to just saying "you must install Sage on your machines" and expecting them to just do it), they might be amenable. I could ask...

embray commented 7 years ago

Alternatively, if that proves infeasible, all of RTD's server / build system software is available and documented. A few years ago I set it up locally in order to try to fix a couple issues I had. I don't remember what went into it, and the setup has probably changed since then. But worse comes to worse we can run our own RTD instance for Sage.

embray commented 7 years ago

In fact, the more I think about it, the more convinced I am that we should just run our own instance. Configuring and installing Sage is not trivial, and ideally we'll want to provide any and all Sage versions a project might want to build their documentation against.

This will undoubtedly spiral beyond what RTD's already overstretched support "staff" (volunteers, really) can handle. We should try setting up and maintaining our own instance for Sage, and only bother them with specific issues with running our own RTD server as they occur. Perhaps William would be willing to help provide hosting for such a site....

nthiery commented 7 years ago

On Mon, Jan 16, 2017 at 05:43:30AM -0800, Erik Bray wrote:

In fact, the more I think about it, the more convinced I am that we should just run our own instance. Configuring and installing Sage is not trivial, and ideally we'll want to provide any and all Sage versions a project might want to build their documentation against.

This will undoubtedly spiral beyond what RTD's already overstretched support "staff" (volunteers, really) can handle. We should try setting up and maintaining our own instance for Sage, and only bother them with specific issues with running our own RTD server as they occur. Perhaps William would be willing to help provide hosting for such a site....

Thanks Erik for the feedback! Very informed as usual :-)

embray commented 7 years ago

Perhaps I'll go ahead and try setting up a RTD server on our OpenStack infrastructure and see how it goes.

(Unrelatedly, I finally figured out how to get to their helpdesk, and have asked them to open the remote management ports needed for Windows; we'll see what they say...)

nthiery commented 7 years ago

On Mon, Jan 16, 2017 at 08:19:50AM -0800, Erik Bray wrote:

Perhaps I'll go ahead and try setting up a RTD server on our OpenStack infrastructure and see how it goes.

(Unrelatedly, I finally figured out how to get to their helpdesk, and have asked them to open the remote management ports needed for Windows; we'll see what they say...)

Cool!

mkoeppe commented 7 years ago

It would be easy to extend the Travis CI scripts so that they deploy the built documentation to github pages (see #8)

koffie commented 7 years ago

@embray I think making all of sage available on the RTD server just so we can build the documentation of sage packages is extreme overkill. The part of sage that is needed in order to build the documentation is actually really small. If we would want this then we should put the relevant documentation building code of sage in a separate python package on which but sage and sage packages can depend for documentation building.

nthiery commented 7 years ago

On Tue, Sep 05, 2017 at 02:12:22PM -0700, Maarten Derickx wrote:

[1]@embray I think making all of sage available on the RTD server just so we can build the documentation of sage packages is extreme overkill. The part of sage that is needed in order to build the documentation is actually really small. If we would want this then we should put the relevant documentation building code of sage in a separate python package on which but sage and sage packages can depend for documentation building.

Just as a data point in this direction: in https://github.com/sagemath/more-sagemath-tutorials/, the documentation is built by Sphinx on RTD, with support of a certain number of the Sphinx features for Sage's documentation (cross references to Sage and Python's documentation, special roles, ...).

What made it easier is that, by nature, this repo consists mostly of ReST files. Where things become harder is when documenting a module where a lot of the documentation resides in the source files. Which requires Sphinx to import them in the first place. Which in turn breaks if Sage is imported but not available.

One workaround would be to create a fake sage module, faking all the submodules sage.** and their content. At some point last Spring, I pondered whether one could create in Python a fake module "foo" which would lazily generate fake submodules (and fake functions/classes/...) whenever they would be imported/use.

koffie commented 7 years ago

Yes this faking of dependencies is certainly possible and actually quite a common problem on RTD. They also kindly provide a solution to this in their FAQ

nthiery commented 7 years ago

Yes this faking of dependencies is certainly possible and actually quite a common problem on RTD. They also kindly provide a solution to this in their [1]FAQ

Right; I should have been more specific. What I am missing in the MagicMock library is a recursive feature, because mocking all sage modules sounds like a pain.

In [1]: import sys
   ...: from unittest.mock import MagicMock
In [2]: class Mock(MagicMock):
   ...:     @classmethod
   ...:     def __getattr__(cls, name):
   ...:             return MagicMock()
In [3]: MOCK_MODULES = ['sage']
In [4]: sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
In [5]: import sage
In [7]: from sage import Blah
In [8]: class A(Blah):
   ...:     pass

So far that's great. But I would like to further do:

In [6]: import sage.foo
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
<ipython-input-6-4ecbf9786377> in <module>()
----> 1 import sage.foo
ModuleNotFoundError: No module named 'sage.foo'
In [10]: from sage.foo import Blah

Well, maybe that's not so bad after all, and we could just have one big list in sage_package that would get updated from time to time:

MOCK_MODULES = ['sage', 'sage.foo', 'sage.foo.bar', ...]

Cheers,

koffie commented 7 years ago

You are right that mocking all sage modules would be a huge pain. But a package would only need to mock the sage modules it is actually importing from. That being said, I tried what I could do by hooking the import mechanism of python. And it seems you can solve it this way:

from mock import MagicMock

class MockedModule(MagicMock):
    __all__=[]
    @classmethod
    def __getattr__(cls, name):
        return MagicMock()

mocked_module = MockedModule()

class MockImporter(object):
    def __init__(self, mocked_modules = []):
        self.mocked_modules = mocked_modules

    def find_module(self, name, path=None):
        if any(name == m or name.startswith(m+'.') for m in self.mocked_modules):
            return self
        return None

    def load_module(self, name):
        return mocked_module

sys.meta_path=[MockImporter(["a"])]
import a.v.g.d.s.f
from a.hs.sdfhd.gs import *
from a.adgagds.gad.agd import l

import random
import non_existing_agsgasdga

As it shows, you can still import existing modules, and raises an error on non existing modules. But only the submodules of "a" are imported. This handles all forms of imports. But like the original workaround on RTD it doesn't work if some code is doing:

from a import *
some_function_imported_from_a()

since there is no way to know at the point of the from a import * statement which names are actually expected to be imported. And returning a list of all possible names is also not possible.

koffie commented 7 years ago

So we should encourage people to not use * imports, which is always a good idea.

nthiery commented 7 years ago

You are right that mocking all sage modules would be a huge pain. But a package would only need to mock the sage modules it is actually importing from. That being said, I tried what I could do by hooking the import mechanism of python. And it seems you can solve it this way: ... sys.meta_path=[MockImporter(["a"])] import a.v.g.d.s.f from a.hs.sdfhd.gs import * from a.adgagds.gad.agd import l

Ah ah! That's exactly what I was dreaming of! Nicely done :-)

I am curious to see how this idea behaves in production. Putting sage_sample on RTD would be a good first test. Do you have a chance?

Putting my sage-semigroups package on RTD would be a serious check given all the recursive monkey patching tricks there. I am not sure when I'll get to that any time soon though.

But like the original workaround on RTD it doesn't work if some code is doing: from a import * some_function_imported_from_a()

Fair enough. As you say, I don't mind discouraging the use of * imports :-)

saraedum commented 6 years ago

There's one minor issues with this, namely Sphinx won't generate documentation for classes that inherit from a MagicMock. I am not sure why exactly but something needs to be disabled on the mocks to make this fully work.

embray commented 6 years ago

To quote from my e-mail just now:

RTD has changed a little bit lately. I haven't configured a new project since those changes so I am not very familiar. But they now use Docker containers as the build environments (this did not used to be the case, IIRC, so it was more restrictive). You can configure which version of the RTD container image to build from. I don't know if it's possible to supply an arbitrary custom image. If it is, it would have to at least be compatible in some way with their official images (sort of like how Binder images have certain interface requirements). I couldn't find any documentation on how to set that up though; I don't know that it's a supported use case (yet).

embray commented 6 years ago

My point is, in principle, mocking might not be necessary at all if you can create docker image based on RTD's official images but that includes Sage. I don't know for sure yet how feasible that is.

saraedum commented 6 years ago

Ok. Let's move this discussion here rather than into these private emails ;) If I understand the documentation correctly, you don't get to specify arbitrary docker images but have to pick from a small list of rtd images. Installing sagelib from conda-forge might be an option. RTD supports environment.yml files, I haven't tried but it could work.

saraedum commented 5 years ago

See https://github.com/MCLF/mclf/pull/107 for an attempt to build with conda-forge on RTD.

saraedum commented 5 years ago

I did not manage to get this to work. Running conda install sagelib exceeds the RAM limits it seems. Probably a newer version of conda could help here. I also see segfaults when I try to reproduce this locally with the readthedocs docker images (which might be the reason for errors without any further information on the readthedocs interface.)

saraedum commented 5 years ago

See https://github.com/rtfd/readthedocs-docker-images/issues/83.