1# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX
2# All rights reserved.
3#
4# This software is provided without warranty under the terms of the BSD
5# license included in LICENSE.txt and may be redistributed only under
6# the conditions described in the aforementioned license. The license
7# is also available online at http://www.enthought.com/licenses/BSD.txt
8#
9# Thanks for using Enthought open source!
10
11""" Tests for the trait documenter. """
12
13import contextlib
14import io
15import os
16import shutil
17import tempfile
18import textwrap
19import tokenize
20import unittest
21import unittest.mock as mock
22
23from traits.api import Bool, HasTraits, Int, Property
24from traits.testing.optional_dependencies import sphinx, requires_sphinx
25
26
27if sphinx is not None:
28    from sphinx.ext.autodoc import ClassDocumenter, INSTANCEATTR, Options
29    from sphinx.ext.autodoc.directive import DocumenterBridge
30    from sphinx.testing.path import path
31    from sphinx.testing.util import SphinxTestApp
32    from sphinx.util.docutils import LoggingReporter
33
34    from traits.util.trait_documenter import (
35        _get_definition_tokens,
36        trait_definition,
37        TraitDocumenter,
38    )
39
40
41# Configuration file content for testing.
42CONF_PY = """\
43extensions = ['sphinx.ext.autodoc']
44
45# The suffix of source filenames.
46source_suffix = '.rst'
47
48autodoc_mock_imports = [
49    'dummy'
50]
51"""
52
53
54class MyTestClass(HasTraits):
55    """
56    Class-level docstring.
57    """
58    #: I'm a troublesome trait with a long definition.
59    bar = Int(42, desc=""" First line
60
61        The answer to
62        Life,
63        the Universe,
64
65        and Everything.
66    """)
67
68
69class Fake(HasTraits):
70
71    #: Test attribute
72    test_attribute = Property(Bool, label="ミスあり")
73
74
75class FindTheTraits(HasTraits):
76    """
77    Class for testing the can_document_member functionality.
78    """
79
80    #: A TraitType subclass on the right-hand side.
81    an_int = Int
82
83    #: A TraitType instance on the right-hand side.
84    another_int = Int()
85
86    #: A non-trait integer
87    magic_number = 1729
88
89    @property
90    def not_a_trait(self):
91        """
92        I'm a regular property, not a trait.
93        """
94
95
96@requires_sphinx
97class TestTraitDocumenter(unittest.TestCase):
98    """ Tests for the trait documenter. """
99
100    def setUp(self):
101        self.source = """
102    depth_interval = Property(Tuple(Float, Float),
103                              depends_on="_depth_interval")
104"""
105        string_io = io.StringIO(self.source)
106        tokens = tokenize.generate_tokens(string_io.readline)
107        self.tokens = tokens
108
109    def test_get_definition_tokens(self):
110        src = textwrap.dedent(
111            """\
112        depth_interval = Property(Tuple(Float, Float),
113                                  depends_on="_depth_interval")
114        """
115        )
116        string_io = io.StringIO(src)
117        tokens = tokenize.generate_tokens(string_io.readline)
118
119        definition_tokens = _get_definition_tokens(tokens)
120
121        # Check if they are correctly untokenized. This should not raise.
122        string = tokenize.untokenize(definition_tokens)
123
124        self.assertEqual(src.rstrip(), string)
125
126    def test_add_line(self):
127
128        mocked_directive = mock.MagicMock()
129
130        documenter = TraitDocumenter(mocked_directive, "test", "   ")
131        documenter.object_name = "test_attribute"
132        documenter.parent = Fake
133
134        with mock.patch(
135            (
136                "traits.util.trait_documenter.ClassLevelDocumenter"
137                ".add_directive_header"
138            )
139        ):
140            documenter.add_directive_header("")
141
142        self.assertEqual(
143            len(documenter.directive.result.append.mock_calls), 1)
144
145    def test_abbreviated_annotations(self):
146        # Regression test for enthought/traits#493.
147        with self.create_directive() as directive:
148            documenter = TraitDocumenter(
149                directive, __name__ + ".MyTestClass.bar")
150            documenter.generate(all_members=True)
151            result = directive.result
152
153        # Find annotations line.
154        for item in result:
155            if item.lstrip().startswith(":annotation:"):
156                break
157        else:
158            self.fail("Didn't find the expected trait :annotation:")
159
160        # Annotation should be a single line.
161        self.assertIn("First line", item)
162        self.assertNotIn("\n", item)
163
164    def test_successful_trait_definition(self):
165        definition = trait_definition(cls=Fake, trait_name="test_attribute")
166        self.assertEqual(
167            definition, 'Property(Bool, label="ミスあり")',
168        )
169
170    def test_failed_trait_definition(self):
171        with self.assertRaises(ValueError):
172            trait_definition(cls=Fake, trait_name="not_a_trait")
173
174    def test_can_document_member(self):
175        # Regression test for enthought/traits#1238
176
177        with self.create_directive() as directive:
178            class_documenter = ClassDocumenter(
179                directive, __name__ + ".FindTheTraits"
180            )
181            class_documenter.parse_name()
182            class_documenter.import_object()
183
184            self.assertTrue(
185                TraitDocumenter.can_document_member(
186                    INSTANCEATTR, "an_int", True, class_documenter,
187                )
188            )
189
190            self.assertTrue(
191                TraitDocumenter.can_document_member(
192                    INSTANCEATTR, "another_int", True, class_documenter,
193                )
194            )
195
196            self.assertFalse(
197                TraitDocumenter.can_document_member(
198                    INSTANCEATTR, "magic_number", True, class_documenter,
199                )
200            )
201
202            self.assertFalse(
203                TraitDocumenter.can_document_member(
204                    INSTANCEATTR, "not_a_trait", True, class_documenter,
205                )
206            )
207
208    @contextlib.contextmanager
209    def create_directive(self):
210        """
211        Helper function to create a a "directive" suitable
212        for instantiating the TraitDocumenter with, along with resources
213        to support that directive, and clean up the resources afterwards.
214
215        Returns
216        -------
217        contextmanager
218            A context manager that returns a DocumenterBridge instance.
219        """
220        with self.tmpdir() as tmpdir:
221            # Ensure configuration file exists.
222            conf_file = os.path.join(tmpdir, "conf.py")
223            with open(conf_file, "w", encoding="utf-8") as f:
224                f.write(CONF_PY)
225
226            app = SphinxTestApp(srcdir=path(tmpdir))
227            app.builder.env.app = app
228            app.builder.env.temp_data["docname"] = "dummy"
229
230            kwds = {}
231            state = mock.Mock()
232            state.document.settings.tab_width = 8
233            kwds["state"] = state
234            yield DocumenterBridge(
235                app.env, LoggingReporter(''), Options(), 1, **kwds)
236
237    @contextlib.contextmanager
238    def tmpdir(self):
239        """
240        Helper function to create a temporary directory.
241
242        Returns
243        -------
244        contextmanager
245            Context manager that returns the path to a temporary directory.
246        """
247        tmpdir = tempfile.mkdtemp()
248        try:
249            yield tmpdir
250        finally:
251            shutil.rmtree(tmpdir)
252