Пример #1
0
class AnonymousCitationTest(TestCase):
    def setUp(self) -> None:
        self._translations = Translations(gettext.NullTranslations())
        self._translations.install()

    def tearDown(self) -> None:
        self._translations.uninstall()

    def test_location(self):
        source = Mock(Source)
        self.assertIsInstance(AnonymousCitation(source).location, str)

    def test_replace(self):
        class _HasCitations(HasCitations, Entity):
            pass

        facts = [_HasCitations()]
        files = [File('F1', __file__)]
        source = Mock(Source)
        sut = AnonymousCitation(source)
        other = AnonymousCitation(source)
        other.facts = facts
        other.files = files
        sut.replace(other)
        self.assertEquals(facts, list(sut.facts))
        self.assertEquals(files, list(sut.files))
Пример #2
0
class AnonymizeSourceTest(TestCase):
    def setUp(self) -> None:
        self._translations = Translations(gettext.NullTranslations())
        self._translations.install()

    def tearDown(self) -> None:
        self._translations.uninstall()

    def test_should_remove_citations(self) -> None:
        source = Source('S0', 'The Source')
        citation = Citation(None, source)
        source.citations.append(citation)
        anonymous_source = AnonymousSource()
        anonymize_source(source, anonymous_source)
        self.assertEquals(0, len(source.citations))
        self.assertIn(citation, anonymous_source.citations)

    def test_should_remove_contained_by(self) -> None:
        source = Source('S0', 'The Source')
        contained_by = Source(None, 'The Source')
        source.contained_by = contained_by
        anonymous_source = AnonymousSource()
        anonymize_source(source, anonymous_source)
        self.assertIsNone(source.contained_by)

    def test_should_remove_contains(self) -> None:
        source = Source('S0', 'The Source')
        contains = Source(None, 'The Source')
        source.contains.append(contains)
        anonymous_source = AnonymousSource()
        anonymize_source(source, anonymous_source)
        self.assertEquals(0, len(source.contains))
        self.assertIn(contains, anonymous_source.contains)

    def test_should_remove_files(self) -> None:
        source = Source('S0', 'The Source')
        file = File('F0', __file__)
        source.files.append(file)
        anonymous_source = AnonymousSource()
        anonymize_source(source, anonymous_source)
        self.assertEquals(0, len(source.files))
        self.assertIn(file, anonymous_source.files)
Пример #3
0
class AnonymizeCitationTest(TestCase):
    def setUp(self) -> None:
        self._translations = Translations(gettext.NullTranslations())
        self._translations.install()

    def tearDown(self) -> None:
        self._translations.uninstall()

    def test_should_remove_facts(self) -> None:
        source = Source('The Source')
        citation = Citation('C0', source)
        fact = PersonName(Person(None), 'Jane')
        citation.facts.append(fact)
        anonymous_source = AnonymousSource()
        anonymous_citation = AnonymousCitation(anonymous_source)
        anonymize_citation(citation, anonymous_citation)
        self.assertEquals(0, len(citation.facts))
        self.assertIn(fact, anonymous_citation.facts)

    def test_should_remove_files(self) -> None:
        source = Source('The Source')
        citation = Citation('C0', source)
        file = File('F0', __file__)
        citation.files.append(file)
        anonymous_source = AnonymousSource()
        anonymous_citation = AnonymousCitation(anonymous_source)
        anonymize_citation(citation, anonymous_citation)
        self.assertEquals(0, len(citation.files))
        self.assertIn(file, anonymous_citation.files)

    def test_should_remove_source(self) -> None:
        source = Source('The Source')
        citation = Citation('C0', source)
        anonymous_source = AnonymousSource()
        anonymous_citation = AnonymousCitation(anonymous_source)
        anonymize_citation(citation, anonymous_citation)
        self.assertIsNone(citation.source)
Пример #4
0
class AnonymousSourceTest(TestCase):
    def setUp(self) -> None:
        self._translations = Translations(gettext.NullTranslations())
        self._translations.install()

    def tearDown(self) -> None:
        self._translations.uninstall()

    def test_name(self):
        self.assertIsInstance(AnonymousSource().name, str)

    def test_replace(self):
        citations = [Citation(None, Source(None))]
        contains = [Source(None)]
        files = [Mock(File)]
        sut = AnonymousSource()
        other = AnonymousSource()
        other.citations = citations
        other.contains = contains
        other.files = files
        sut.replace(other)
        self.assertEquals(citations, list(sut.citations))
        self.assertEquals(contains, list(sut.contains))
        self.assertEquals(files, list(sut.files))
Пример #5
0
class Site:
    def __init__(self, configuration: Configuration):
        self._site_stack = []
        self._ancestry = Ancestry()
        self._configuration = configuration
        self._assets = FileSystem(join(dirname(abspath(__file__)), 'assets'))
        self._dispatcher = Dispatcher()
        self._localized_url_generator = SiteUrlGenerator(configuration)
        self._static_url_generator = StaticPathUrlGenerator(configuration)
        self._locale = None
        self._translations = defaultdict(gettext.NullTranslations)
        self._default_translations = None
        self._plugins = OrderedDict()
        self._plugin_exit_stack = AsyncExitStack()
        self._init_plugins()
        self._init_dispatch_handlers()
        self._init_assets()
        self._init_translations()
        self._jinja2_environment = None
        self._renderer = None
        self._executor = None
        self._locks = Locks()

    async def __aenter__(self):
        if not self._site_stack:
            for plugin in self._plugins.values():
                await self._plugin_exit_stack.enter_async_context(plugin)

        self._default_translations = Translations(
            self.translations[self.locale])
        self._default_translations.install()

        if self._executor is None:
            self._executor = ExceptionRaisingExecutor(ProcessPoolExecutor())

        self._site_stack.append(self)

        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        self._site_stack.pop()

        self._default_translations.uninstall()

        if not self._site_stack:
            self._executor.shutdown()
            self._executor = None
            await self._plugin_exit_stack.aclose()

    @property
    def locale(self) -> str:
        if self._locale is not None:
            return self._locale
        return self._configuration.default_locale

    def _init_plugins(self) -> None:
        from betty.plugin import NO_CONFIGURATION

        def _extend_plugin_type_graph(graph: Graph,
                                      plugin_type: Type['Plugin']):
            dependencies = plugin_type.depends_on()
            # Ensure each plugin type appears in the graph, even if they're isolated.
            graph.setdefault(plugin_type, set())
            for dependency in dependencies:
                seen_dependency = dependency in graph
                graph[dependency].add(plugin_type)
                if not seen_dependency:
                    _extend_plugin_type_graph(graph, dependency)

        plugin_types_graph = defaultdict(set)
        # Add dependencies to the plugin graph.
        for plugin_type, _ in self._configuration.plugins:
            _extend_plugin_type_graph(plugin_types_graph, plugin_type)
        # Now all dependencies have been collected, extend the graph with optional plugin orders.
        for plugin_type, _ in self._configuration.plugins:
            for before in plugin_type.comes_before():
                if before in plugin_types_graph:
                    plugin_types_graph[plugin_type].add(before)
            for after in plugin_type.comes_after():
                if after in plugin_types_graph:
                    plugin_types_graph[after].add(plugin_type)

        for plugin_type in tsort(plugin_types_graph):
            plugin_configuration = self.configuration.plugins[
                plugin_type] if plugin_type in self.configuration.plugins else NO_CONFIGURATION
            plugin = plugin_type.for_site(self, plugin_configuration)
            self._plugins[plugin_type] = plugin

    def _init_dispatch_handlers(self) -> None:
        for plugin in self._plugins.values():
            self._dispatcher.append_handler(plugin)

    def _init_assets(self) -> None:
        for plugin in self._plugins.values():
            if plugin.assets_directory_path is not None:
                self._assets.paths.appendleft(plugin.assets_directory_path)
        if self._configuration.assets_directory_path:
            self._assets.paths.appendleft(
                self._configuration.assets_directory_path)

    def _init_translations(self) -> None:
        self._translations['en-US'] = gettext.NullTranslations()
        for locale in self._configuration.locales:
            for assets_path in reversed(self._assets.paths):
                translations = open_translations(locale, assets_path)
                if translations:
                    translations.add_fallback(self._translations[locale])
                    self._translations[locale] = translations

    @property
    def ancestry(self) -> Ancestry:
        return self._ancestry

    @property
    def configuration(self) -> Configuration:
        return self._configuration

    @property
    def plugins(self) -> Dict[Type['Plugin'], 'Plugin']:
        return self._plugins

    @property
    def assets(self) -> FileSystem:
        return self._assets

    @property
    def dispatcher(self) -> Dispatcher:
        return self._dispatcher

    @property
    def localized_url_generator(self) -> LocalizedUrlGenerator:
        return self._localized_url_generator

    @property
    def static_url_generator(self) -> StaticUrlGenerator:
        return self._static_url_generator

    @property
    def translations(self) -> Dict[str, gettext.NullTranslations]:
        return self._translations

    @property
    def jinja2_environment(self) -> Environment:
        if not self._jinja2_environment:
            from betty.jinja2 import BettyEnvironment
            self._jinja2_environment = BettyEnvironment(self)

        return self._jinja2_environment

    @property
    def renderer(self) -> Renderer:
        if not self._renderer:
            from betty.jinja2 import Jinja2Renderer
            self._renderer = SequentialRenderer([
                Jinja2Renderer(self.jinja2_environment, self._configuration),
                SassRenderer(),
            ])

        return self._renderer

    @property
    def executor(self) -> Executor:
        if self._executor is None:
            raise RuntimeError(
                "Cannot get the executor before this site's context is entered."
            )
        return self._executor

    @property
    def locks(self) -> Locks:
        return self._locks

    def with_locale(self, locale: str) -> 'Site':
        locale = negotiate_locale(locale,
                                  list(self.configuration.locales.keys()))
        if locale is None:
            raise ValueError('Locale "%s" is not enabled.' % locale)
        if locale == self.locale:
            return self

        site = copy(self)
        site._locale = locale

        # Clear all locale-dependent lazy-loaded attributes.
        site._jinja2_environment = None
        site._renderer = None

        return site
Пример #6
0
class AnonymizeTest(TestCase):
    def setUp(self) -> None:
        self._translations = Translations(gettext.NullTranslations())
        self._translations.install()

    def tearDown(self) -> None:
        self._translations.uninstall()

    @patch('betty.extension.anonymizer.anonymize_person')
    def test_with_public_person_should_not_anonymize(
            self, m_anonymize_person) -> None:
        person = Person('P0')
        person.private = False
        ancestry = Ancestry()
        ancestry.entities.append(person)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_person.assert_not_called()

    @patch('betty.extension.anonymizer.anonymize_person')
    def test_with_private_person_should_anonymize(self,
                                                  m_anonymize_person) -> None:
        person = Person('P0')
        person.private = True
        ancestry = Ancestry()
        ancestry.entities.append(person)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_person.assert_called_once_with(person)

    @patch('betty.extension.anonymizer.anonymize_event')
    def test_with_public_event_should_not_anonymize(self,
                                                    m_anonymize_event) -> None:
        event = Event('E0', Birth())
        event.private = False
        ancestry = Ancestry()
        ancestry.entities.append(event)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_event.assert_not_called()

    @patch('betty.extension.anonymizer.anonymize_event')
    def test_with_private_event_should_anonymize(self,
                                                 m_anonymize_event) -> None:
        event = Event('E0', Birth())
        event.private = True
        ancestry = Ancestry()
        ancestry.entities.append(event)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_event.assert_called_once_with(event)

    @patch('betty.extension.anonymizer.anonymize_file')
    def test_with_public_file_should_not_anonymize(self,
                                                   m_anonymize_file) -> None:
        file = File('F0', __file__)
        file.private = False
        ancestry = Ancestry()
        ancestry.entities.append(file)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_file.assert_not_called()

    @patch('betty.extension.anonymizer.anonymize_file')
    def test_with_private_file_should_anonymize(self,
                                                m_anonymize_file) -> None:
        file = File('F0', __file__)
        file.private = True
        ancestry = Ancestry()
        ancestry.entities.append(file)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_file.assert_called_once_with(file)

    @patch('betty.extension.anonymizer.anonymize_source')
    def test_with_public_source_should_not_anonymize(
            self, m_anonymize_source) -> None:
        source = Source('S0', 'The Source')
        source.private = False
        ancestry = Ancestry()
        ancestry.entities.append(source)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_source.assert_not_called()

    @patch('betty.extension.anonymizer.anonymize_source')
    def test_with_private_source_should_anonymize(self,
                                                  m_anonymize_source) -> None:
        source = Source('S0', 'The Source')
        source.private = True
        ancestry = Ancestry()
        ancestry.entities.append(source)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_source.assert_called_once_with(source, ANY)

    @patch('betty.extension.anonymizer.anonymize_citation')
    def test_with_public_citation_should_not_anonymize(
            self, m_anonymize_citation) -> None:
        source = Source('The Source')
        citation = Citation('C0', source)
        citation.private = False
        ancestry = Ancestry()
        ancestry.entities.append(citation)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_citation.assert_not_called()

    @patch('betty.extension.anonymizer.anonymize_citation')
    def test_with_private_citation_should_anonymize(
            self, m_anonymize_citation) -> None:
        source = Source('The Source')
        citation = Citation('C0', source)
        citation.private = True
        ancestry = Ancestry()
        ancestry.entities.append(citation)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_citation.assert_called_once_with(citation, ANY)
Пример #7
0
class App:
    def __init__(self, configuration: Configuration):
        self._app_stack = []
        self._ancestry = Ancestry()
        self._configuration = configuration
        self._assets = FileSystem(join(dirname(abspath(__file__)), 'assets'))
        self._dispatcher = None
        self._localized_url_generator = AppUrlGenerator(configuration)
        self._static_url_generator = StaticPathUrlGenerator(configuration)
        self._locale = None
        self._translations = defaultdict(gettext.NullTranslations)
        self._default_translations = None
        self._extensions = OrderedDict()
        self._extension_exit_stack = AsyncExitStack()
        self._init_extensions()
        self._init_dispatcher()
        self._init_assets()
        self._init_translations()
        self._jinja2_environment = None
        self._renderer = None
        self._executor = None
        self._locks = Locks()

    async def enter(self):
        if not self._app_stack:
            for extension in self._extensions.values():
                await self._extension_exit_stack.enter_async_context(extension)

        self._default_translations = Translations(
            self.translations[self.locale])
        self._default_translations.install()

        if self._executor is None:
            self._executor = ExceptionRaisingExecutor(ThreadPoolExecutor())

        self._app_stack.append(self)

        return self

    async def exit(self):
        self._app_stack.pop()

        self._default_translations.uninstall()

        if not self._app_stack:
            self._executor.shutdown()
            self._executor = None
            await self._extension_exit_stack.aclose()

    async def __aenter__(self) -> 'App':
        return await self.enter()

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.exit()

    @property
    def locale(self) -> str:
        if self._locale is not None:
            return self._locale
        return self._configuration.default_locale

    def _init_extensions(self) -> None:
        for grouped_extension_types in tsort_grouped(
                build_extension_type_graph(
                    set(self._configuration.extensions.keys()))):
            for extension_type in grouped_extension_types:
                extension_args = []
                if issubclass(
                        extension_type, ConfigurableExtension
                ) and extension_type in self.configuration.extensions:
                    extension_kwargs = self.configuration.extensions[
                        extension_type]
                else:
                    extension_kwargs = {}

                if issubclass(extension_type, AppAwareFactory):
                    extension = extension_type.new_for_app(
                        self, *extension_args, **extension_kwargs)
                else:
                    extension = extension_type(*extension_args,
                                               **extension_kwargs)

                self._extensions[extension_type] = extension

    def _init_dispatcher(self) -> None:
        from betty.extension import ExtensionDispatcher

        self._dispatcher = ExtensionDispatcher(self._extensions.values())

    def _init_assets(self) -> None:
        for extension in self._extensions.values():
            if extension.assets_directory_path is not None:
                self._assets.paths.appendleft(extension.assets_directory_path)
        if self._configuration.assets_directory_path:
            self._assets.paths.appendleft(
                self._configuration.assets_directory_path)

    def _init_translations(self) -> None:
        self._translations['en-US'] = gettext.NullTranslations()
        for locale in self._configuration.locales:
            for assets_path in reversed(self._assets.paths):
                translations = open_translations(locale, assets_path)
                if translations:
                    translations.add_fallback(self._translations[locale])
                    self._translations[locale] = translations

    @property
    def ancestry(self) -> Ancestry:
        return self._ancestry

    @property
    def configuration(self) -> Configuration:
        return self._configuration

    @property
    def extensions(self) -> Dict[Type[Extension], Extension]:
        return self._extensions

    @property
    def assets(self) -> FileSystem:
        return self._assets

    @property
    def dispatcher(self) -> Dispatcher:
        return self._dispatcher

    @property
    def localized_url_generator(self) -> LocalizedUrlGenerator:
        return self._localized_url_generator

    @property
    def static_url_generator(self) -> StaticUrlGenerator:
        return self._static_url_generator

    @property
    def translations(self) -> Dict[str, gettext.NullTranslations]:
        return self._translations

    @property
    def jinja2_environment(self) -> Environment:
        if not self._jinja2_environment:
            from betty.jinja2 import BettyEnvironment
            self._jinja2_environment = BettyEnvironment(self)

        return self._jinja2_environment

    @property
    def renderer(self) -> Renderer:
        if not self._renderer:
            from betty.jinja2 import Jinja2Renderer
            self._renderer = SequentialRenderer([
                Jinja2Renderer(self.jinja2_environment, self._configuration),
            ])

        return self._renderer

    @property
    def executor(self) -> Executor:
        if self._executor is None:
            raise RuntimeError(
                "Cannot get the executor before this app's context is entered."
            )
        return self._executor

    @property
    def locks(self) -> Locks:
        return self._locks

    def with_locale(self, locale: str) -> 'App':
        locale = negotiate_locale(locale,
                                  list(self.configuration.locales.keys()))
        if locale is None:
            raise ValueError('Locale "%s" is not enabled.' % locale)
        if locale == self.locale:
            return self

        app = copy(self)
        app._locale = locale

        # Clear all locale-dependent lazy-loaded attributes.
        app._jinja2_environment = None
        app._renderer = None

        return app