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