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))
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)
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)
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))
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
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)
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