diff --git a/docs/dev-guide/plugins.md b/docs/dev-guide/plugins.md index 2d692fc6..f71f0dc7 100644 --- a/docs/dev-guide/plugins.md +++ b/docs/dev-guide/plugins.md @@ -157,11 +157,23 @@ There are three kinds of events: [Global Events], [Page Events] and
-#### Global Events +#### One-time Events -Global events are called once per build at either the beginning or end of the -build process. Any changes made in these events will have a global effect on the -entire site. +One-time events run once per `mkdocs` invocation. The only case where these tangibly differ from [global events](#global-events) is for `mkdocs serve`: global events, unlike these, will run multiple times -- once per *build*. + +##### on_startup + +::: mkdocs.plugins.BasePlugin.on_startup + options: + show_root_heading: false + show_root_toc_entry: false + +##### on_shutdown + +::: mkdocs.plugins.BasePlugin.on_shutdown + options: + show_root_heading: false + show_root_toc_entry: false ##### on_serve @@ -170,6 +182,12 @@ entire site. show_root_heading: false show_root_toc_entry: false +#### Global Events + +Global events are called once per build at either the beginning or end of the +build process. Any changes made in these events will have a global effect on the +entire site. + ##### on_config ::: mkdocs.plugins.BasePlugin.on_config diff --git a/docs/img/plugin-events.py b/docs/img/plugin-events.py index 86fc1484..8e8cfbc4 100644 --- a/docs/img/plugin-events.py +++ b/docs/img/plugin-events.py @@ -7,11 +7,11 @@ import re from graphviz import Digraph -g = Digraph("MkDocs", format="svg") -g.attr(compound="true", bgcolor="transparent") -g.graph_attr.update(fontname="inherit", tooltip=" ") -g.node_attr.update(fontname="inherit", tooltip=" ", style="filled") -g.edge_attr.update(fontname="inherit", tooltip=" ") +graph = Digraph("MkDocs", format="svg") +graph.attr(compound="true", bgcolor="transparent") +graph.graph_attr.update(fontname="inherit", tooltip=" ") +graph.node_attr.update(fontname="inherit", tooltip=" ", style="filled") +graph.edge_attr.update(fontname="inherit", tooltip=" ") def strip_suffix(name): @@ -60,7 +60,7 @@ def edge(g, a, b, dashed=False, **kwargs): def ensure_order(a, b): - edge(g, a, b, style="invis") + edge(graph, a, b, style="invis") @contextlib.contextmanager @@ -79,7 +79,7 @@ def event(g, name, parameters): g, f"cluster_{name}", href=f"#{name}", bgcolor="#ffff3388", pencolor="#00000088" ) as c: label = "|".join(f"<{p}>{p}" for p in parameters.split()) - node(c, name, shape="record", label=label, fillcolor="#ffffff55") + node(c, name, shape="record" if parameters else "point", label=label, fillcolor="#ffffff55") def placeholder_cluster(g, name): @@ -87,72 +87,74 @@ def placeholder_cluster(g, name): node(c, f"placeholder_{name}", label="...", fillcolor="transparent", color="transparent") -event(g, "on_config", "config") -event(g, "on_pre_build", "config") -event(g, "on_files", "files config") -event(g, "on_nav", "nav config files") +event(graph, "on_startup", "command") -edge(g, "load_config", "on_config:config") -edge(g, "on_config:config", "on_pre_build:config") -edge(g, "on_config:config", "get_files") -edge(g, "get_files", "on_files:files") -edge(g, "on_files:files", "get_nav") -edge(g, "get_nav", "on_nav:nav") -edge(g, "on_files:files", "on_nav:files") +with cluster(graph, "cluster_build", bgcolor="#dddddd11") as g: + event(g, "on_config", "config") + event(g, "on_pre_build", "config") + event(g, "on_files", "files config") + event(g, "on_nav", "nav config files") -with cluster(g, "cluster_populate_page") as c: - event(c, "on_pre_page", "page config files") - event(c, "on_page_read_source", "page config") - event(c, "on_page_markdown", "markdown page config files") - event(c, "on_page_content", "html page config files") + edge(g, "load_config", "on_config:config") + edge(g, "on_config:config", "on_pre_build:config") + edge(g, "on_config:config", "get_files") + edge(g, "get_files", "on_files:files") + edge(g, "on_files:files", "get_nav") + edge(g, "get_nav", "on_nav:nav") + edge(g, "on_files:files", "on_nav:files") - edge(c, "on_pre_page:page", "on_page_read_source:page", style="dashed") - edge(c, "cluster_on_page_read_source", "on_page_markdown:markdown", style="dashed") - edge(c, "on_page_markdown:markdown", "render_p", style="dashed") - edge(c, "render_p", "on_page_content:html", style="dashed") + with cluster(g, "cluster_populate_page") as c: + event(c, "on_pre_page", "page config files") + event(c, "on_page_read_source", "page config") + event(c, "on_page_markdown", "markdown page config files") + event(c, "on_page_content", "html page config files") -edge(g, "on_nav:files", "pages_point_a", arrowhead="none") -edge(g, "pages_point_a", "on_pre_page:page", style="dashed") -edge(g, "pages_point_a", "cluster_populate_page") + edge(c, "on_pre_page:page", "on_page_read_source:page", style="dashed") + edge(c, "cluster_on_page_read_source", "on_page_markdown:markdown", style="dashed") + edge(c, "on_page_markdown:markdown", "render_p", style="dashed") + edge(c, "render_p", "on_page_content:html", style="dashed") -for i in 2, 3: - placeholder_cluster(g, f"cluster_populate_page_{i}") - edge(g, "pages_point_a", f"cluster_populate_page_{i}", style="dashed") - edge(g, f"cluster_populate_page_{i}", "pages_point_b", style="dashed") - - -event(g, "on_env", "env config files") - -edge(g, "on_page_content:html", "pages_point_b", style="dashed") -edge(g, "pages_point_b", "on_env:files") - - -edge(g, "pages_point_b", "pages_point_c", arrowhead="none") -edge(g, "pages_point_c", "on_page_context:page", style="dashed") - -with cluster(g, "cluster_build_page") as c: - event(c, "on_page_context", "context page config nav") - event(c, "on_post_page", "output page config") - - edge(c, "get_context", "on_page_context:context") - edge(c, "on_page_context:context", "render") - edge(c, "get_template", "render") - edge(c, "render", "on_post_page:output") - edge(c, "on_post_page:output", "write_file") - - -edge(g, "on_nav:nav", "cluster_build_page") -edge(g, "on_env:env", "cluster_build_page") - -for i in 2, 3: - placeholder_cluster(g, f"cluster_build_page_{i}") - edge(g, "pages_point_c", f"cluster_build_page_{i}", style="dashed") - - -event(g, "on_post_build", "config") -event(g, "on_serve", "server config") + edge(g, "on_nav:files", "pages_point_a", arrowhead="none") + edge(g, "pages_point_a", "on_pre_page:page", style="dashed") + edge(g, "pages_point_a", "cluster_populate_page") + + for i in 2, 3: + placeholder_cluster(g, f"cluster_populate_page_{i}") + edge(g, "pages_point_a", f"cluster_populate_page_{i}", style="dashed") + edge(g, f"cluster_populate_page_{i}", "pages_point_b", style="dashed") + + event(g, "on_env", "env config files") + + edge(g, "on_page_content:html", "pages_point_b", style="dashed") + edge(g, "pages_point_b", "on_env:files") + + edge(g, "pages_point_b", "pages_point_c", arrowhead="none") + edge(g, "pages_point_c", "on_page_context:page", style="dashed") + + with cluster(g, "cluster_build_page") as c: + event(c, "on_page_context", "context page config nav") + event(c, "on_post_page", "output page config") + + edge(c, "get_context", "on_page_context:context") + edge(c, "on_page_context:context", "render") + edge(c, "get_template", "render") + edge(c, "render", "on_post_page:output") + edge(c, "on_post_page:output", "write_file") + + edge(g, "on_nav:nav", "cluster_build_page") + edge(g, "on_env:env", "cluster_build_page") + + for i in 2, 3: + placeholder_cluster(g, f"cluster_build_page_{i}") + edge(g, "pages_point_c", f"cluster_build_page_{i}", style="dashed") + + event(g, "on_post_build", "config") + +event(graph, "on_serve", "server config") +event(graph, "on_shutdown", "") +ensure_order("on_startup", "cluster_build") ensure_order("on_pre_build", "on_files") ensure_order("on_nav", "cluster_populate_page") ensure_order("cluster_populate_page_2", "cluster_populate_page_3") @@ -161,8 +163,9 @@ ensure_order("pages_point_c", "cluster_build_page") ensure_order("cluster_build_page_2", "cluster_build_page_3") ensure_order("cluster_build_page", "on_post_build") ensure_order("on_post_build", "on_serve") +ensure_order("on_serve", "on_shutdown") -data = g.pipe() +data = graph.pipe() data = data[data.index(b" - + + MkDocs -cluster_on_config - - -on_config +cluster_on_startup + + +on_startup -cluster_on_pre_build - - -on_pre_build +cluster_build + + +build -cluster_on_files - - -on_files +cluster_on_config + + +on_config -cluster_on_nav - - -on_nav +cluster_on_pre_build + + +on_pre_build -cluster_populate_page - - -populate_page +cluster_on_files + + +on_files -cluster_on_pre_page - - -on_pre_page +cluster_on_nav + + +on_nav -cluster_on_page_read_source - - -on_page_read_source +cluster_populate_page + + +populate_page -cluster_on_page_markdown - - -on_page_markdown +cluster_on_pre_page + + +on_pre_page -cluster_on_page_content - - -on_page_content - - - - -cluster_on_env - - -on_env +cluster_on_page_read_source + + +on_page_read_source -cluster_populate_page_2 - - -populate_page +cluster_on_page_markdown + + +on_page_markdown -cluster_populate_page_3 - - -populate_page - - - - -cluster_build_page - - -build_page +cluster_on_page_content + + +on_page_content -cluster_on_page_context - - -on_page_context +cluster_on_env + + +on_env + + + + +cluster_populate_page_2 + + +populate_page + + + + +cluster_populate_page_3 + + +populate_page -cluster_on_post_page - - -on_post_page +cluster_build_page + + +build_page -cluster_build_page_2 - - -build_page +cluster_on_page_context + + +on_page_context -cluster_build_page_3 - - -build_page +cluster_on_post_page + + +on_post_page -cluster_on_post_build - - -on_post_build +cluster_build_page_2 + + +build_page -cluster_on_serve - - -on_serve +cluster_build_page_3 + + +build_page - + +cluster_on_post_build + + +on_post_build + + + + +cluster_on_serve + + +on_serve + + + + +cluster_on_shutdown + + +on_shutdown + + + + -on_config +on_startup - -config + +command + + + + + +load_config + + +load_config + + + + + + +on_config + + +config - + on_pre_build - - -config + + +config @@ -179,17 +222,17 @@ on_config:s->on_pre_build:n - - + + - + get_files - - -get_files + + +get_files @@ -197,33 +240,33 @@ on_config:s->get_files - - + + - + on_files - - -files - -config + + +files + +config - + on_nav - - -nav - -config - -files + + +nav + +config + +files @@ -231,17 +274,17 @@ on_files:s->on_nav:n - - + + - + get_nav - - -get_nav + + +get_nav @@ -249,26 +292,26 @@ on_files:s->get_nav - - + + - + render_p - - -render + + +render - + pages_point_a - - + + @@ -276,16 +319,16 @@ on_nav:s->pages_point_a - + - + get_context - - -get_context + + +get_context @@ -293,17 +336,8 @@ on_nav:s->get_context - - - - - - - -load_config - - -load_config + + @@ -311,8 +345,8 @@ load_config->on_config:n - - + + @@ -320,8 +354,8 @@ get_files->on_files:n - - + + @@ -329,32 +363,32 @@ get_nav->on_nav:n - - + + - + on_pre_page - - -page - -config - -files + + +page + +config + +files - + on_page_read_source - - -page - -config + + +page + +config @@ -362,23 +396,23 @@ on_pre_page:s->on_page_read_source:n - - + + - + on_page_markdown - - -markdown - -page - -config - -files + + +markdown + +page + +config + +files @@ -386,8 +420,8 @@ on_page_read_source:s->on_page_markdown:n - - + + @@ -395,31 +429,31 @@ on_page_markdown:s->render_p - - + + - + on_page_content - - -html - -page - -config - -files + + +html + +page + +config + +files - + pages_point_b - - + + @@ -427,21 +461,21 @@ on_page_content:s->pages_point_b - - + + - + on_env - - -env - -config - -files + + +env + +config + +files @@ -450,8 +484,8 @@ render_p->on_page_content:n - - + + @@ -459,8 +493,8 @@ pages_point_a->on_pre_page:n - - + + @@ -468,17 +502,17 @@ pages_point_a->render_p - - + + - + placeholder_cluster_populate_page_2 - - -... + + +... @@ -486,17 +520,17 @@ pages_point_a->placeholder_cluster_populate_page_2:n - - + + - + placeholder_cluster_populate_page_3 - - -... + + +... @@ -504,8 +538,8 @@ pages_point_a->placeholder_cluster_populate_page_3:n - - + + @@ -513,8 +547,8 @@ placeholder_cluster_populate_page_2:s->pages_point_b - - + + @@ -523,16 +557,16 @@ pages_point_b->on_env:n - - + + - + pages_point_c - - + + @@ -540,7 +574,7 @@ pages_point_b->pages_point_c - + @@ -548,8 +582,8 @@ placeholder_cluster_populate_page_3:s->pages_point_b - - + + @@ -557,23 +591,23 @@ on_env:s->get_context - - + + - + on_page_context - - -context - -page - -config - -nav + + +context + +page + +config + +nav @@ -581,18 +615,18 @@ pages_point_c->on_page_context:n - - + + - + placeholder_cluster_build_page_2 - - -... + + +... @@ -600,17 +634,17 @@ pages_point_c->placeholder_cluster_build_page_2:n - - + + - + placeholder_cluster_build_page_3 - - -... + + +... @@ -618,17 +652,17 @@ pages_point_c->placeholder_cluster_build_page_3:n - - + + - + render - - -render + + +render @@ -636,30 +670,30 @@ on_page_context:s->render - - + + - + on_post_page - - -output - -page - -config + + +output + +page + +config - + write_file - - -write_file + + +write_file @@ -667,8 +701,8 @@ on_post_page:s->write_file - - + + @@ -676,8 +710,8 @@ get_context->on_page_context:n - - + + @@ -685,17 +719,17 @@ render->on_post_page:n - - + + - + get_template - - -get_template + + +get_template @@ -703,33 +737,42 @@ get_template->render - - + + - + on_post_build - - -config + + +config - + on_serve - - -server - -config + + +server + +config + + +on_shutdown + + + + + + diff --git a/mkdocs/__main__.py b/mkdocs/__main__.py index bd726cfa..8b6066cb 100644 --- a/mkdocs/__main__.py +++ b/mkdocs/__main__.py @@ -240,7 +240,12 @@ def build_command(clean, **kwargs): from mkdocs.commands import build _enable_warnings() - build.build(config.load_config(**kwargs), dirty=not clean) + cfg = config.load_config(**kwargs) + cfg['plugins'].run_event('startup', command='build') + try: + build.build(cfg, dirty=not clean) + finally: + cfg['plugins'].run_event('shutdown') @cli.command(name="gh-deploy") @@ -263,7 +268,11 @@ def gh_deploy_command( _enable_warnings() cfg = config.load_config(remote_branch=remote_branch, remote_name=remote_name, **kwargs) - build.build(cfg, dirty=not clean) + cfg['plugins'].run_event('startup', command='gh-deploy') + try: + build.build(cfg, dirty=not clean) + finally: + cfg['plugins'].run_event('shutdown') gh_deploy.gh_deploy( cfg, message=message, diff --git a/mkdocs/commands/build.py b/mkdocs/commands/build.py index e8b76fc4..940624c3 100644 --- a/mkdocs/commands/build.py +++ b/mkdocs/commands/build.py @@ -3,6 +3,7 @@ from __future__ import annotations import gzip import logging import os +import time from typing import Any, Dict, Optional, Sequence, Set, Union from urllib.parse import urlsplit @@ -265,8 +266,6 @@ def build(config: Config, live_server: bool = False, dirty: bool = False) -> Non logging.getLogger('mkdocs').addHandler(warning_counter) try: - import time - start = time.monotonic() # Run `config` plugin events. diff --git a/mkdocs/commands/serve.py b/mkdocs/commands/serve.py index 12412ba6..7d44fed6 100644 --- a/mkdocs/commands/serve.py +++ b/mkdocs/commands/serve.py @@ -1,5 +1,6 @@ from __future__ import annotations +import functools import logging import shutil import tempfile @@ -42,17 +43,21 @@ def serve( def mount_path(config): return urlsplit(config['site_url'] or '/').path - def builder(): + get_config = functools.partial( + load_config, + config_file=config_file, + dev_addr=dev_addr, + strict=strict, + theme=theme, + theme_dir=theme_dir, + site_dir=site_dir, + **kwargs, + ) + + def builder(config=None): log.info("Building documentation...") - config = load_config( - config_file=config_file, - dev_addr=dev_addr, - strict=strict, - theme=theme, - theme_dir=theme_dir, - site_dir=site_dir, - **kwargs, - ) + if config is None: + config = get_config() # combine CLI watch arguments with config file values if config["watch"] is None: @@ -66,11 +71,13 @@ def serve( live_server = livereload in ['dirty', 'livereload'] dirty = livereload == 'dirty' build(config, live_server=live_server, dirty=dirty) - return config + + config = get_config() + config['plugins'].run_event('startup', command='serve') try: # Perform the initial build - config = builder() + builder(config) host, port = config['dev_addr'] server = LiveReloadServer( @@ -114,5 +121,6 @@ def serve( # Avoid ugly, unhelpful traceback raise Abort(f'{type(e).__name__}: {e}') finally: + config['plugins'].run_event('shutdown') if isdir(site_dir): shutil.rmtree(site_dir) diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py index 0c421866..caeb9989 100644 --- a/mkdocs/config/config_options.py +++ b/mkdocs/config/config_options.py @@ -5,7 +5,7 @@ import os import sys import traceback import typing as t -from typing import NamedTuple +from typing import Dict, NamedTuple from urllib.parse import urlsplit, urlunsplit import markdown @@ -705,6 +705,7 @@ class Plugins(OptionallyRequired): super().__init__(**kwargs) self.installed_plugins = plugins.get_plugins() self.config_file_path = None + self.plugin_cache: Dict[str, plugins.BasePlugin] = {} def pre_validation(self, config, key_name): self.config_file_path = config.config_file_path @@ -738,15 +739,22 @@ class Plugins(OptionallyRequired): if not isinstance(config, dict): raise ValidationError(f"Invalid config options for the '{name}' plugin.") - Plugin = self.installed_plugins[name].load() + try: + plugin = self.plugin_cache[name] + except KeyError: + Plugin = self.installed_plugins[name].load() - if not issubclass(Plugin, plugins.BasePlugin): - raise ValidationError( - f'{Plugin.__module__}.{Plugin.__name__} must be a subclass of' - f' {plugins.BasePlugin.__module__}.{plugins.BasePlugin.__name__}' - ) + if not issubclass(Plugin, plugins.BasePlugin): + raise ValidationError( + f'{Plugin.__module__}.{Plugin.__name__} must be a subclass of' + f' {plugins.BasePlugin.__module__}.{plugins.BasePlugin.__name__}' + ) + + plugin = Plugin() + + if hasattr(plugin, 'on_startup') or hasattr(plugin, 'on_shutdown'): + self.plugin_cache[name] = plugin - plugin = Plugin() errors, warnings = plugin.load_config(config, self.config_file_path) self.warnings.extend(warnings) errors_message = '\n'.join(f"Plugin '{name}' value: '{x}'. Error: {y}" for x, y in errors) diff --git a/mkdocs/livereload/__init__.py b/mkdocs/livereload/__init__.py index e74f4ff4..c0c99bdf 100644 --- a/mkdocs/livereload/__init__.py +++ b/mkdocs/livereload/__init__.py @@ -171,7 +171,7 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe self._visible_epoch = self._wanted_epoch self._epoch_cond.notify_all() - def shutdown(self) -> None: + def shutdown(self, wait=False) -> None: self.observer.stop() with self._rebuild_cond: self._shutdown = True @@ -179,6 +179,7 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe if self.serve_thread.is_alive(): super().shutdown() + if wait: self.serve_thread.join() self.observer.join() diff --git a/mkdocs/plugins.py b/mkdocs/plugins.py index c7afc758..e7341a38 100644 --- a/mkdocs/plugins.py +++ b/mkdocs/plugins.py @@ -14,6 +14,11 @@ if sys.version_info >= (3, 10): else: from importlib_metadata import EntryPoint, entry_points +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal + import jinja2.environment from mkdocs.config.base import Config, ConfigErrors, ConfigWarnings, PlainConfigSchema @@ -61,16 +66,48 @@ class BasePlugin: return self.config.validate() - # (Note that event implementations shouldn't actually be static methods in subclasses) + # One-time events - # Global events + def on_startup(self, command: Literal['build', 'gh-deploy', 'serve']) -> None: + """ + The `startup` event runs once at the very beginning of an `mkdocs` invocation. + + New in MkDocs 1.4. + + The presence of an `on_startup` method (even if empty) migrates the plugin to the new + system where the plugin object is kept across builds within one `mkdocs serve`. + + Note that for initializing variables, the `__init__` method is still preferred. + For initializing per-build variables (and whenever in doubt), use the `on_config` event. + + Parameters: + command: the command that MkDocs was invoked with, e.g. "serve" for `mkdocs serve`. + """ + + def on_shutdown(self) -> None: + """ + The `shutdown` event runs once at the very end of an `mkdocs` invocation, before exiting. + + This event is relevant only for support of `mkdocs serve`, otherwise within a + single build it's undistinguishable from `on_post_build`. + + New in MkDocs 1.4. + + The presence of an `on_shutdown` method (even if empty) migrates the plugin to the new + system where the plugin object is kept across builds within one `mkdocs serve`. + + Note the `on_post_build` method is still preferred for cleanups, when possible, as it has + a much higher chance of actually triggering. `on_shutdown` is "best effort" because it + relies on detecting a graceful shutdown of MkDocs. + """ def on_serve( self, server: LiveReloadServer, config: Config, builder: Callable ) -> Optional[LiveReloadServer]: """ The `serve` event is only called when the `serve` command is used during - development. It is passed the `Server` instance which can be modified before + development. It runs only once, after the first build finishes. + It is passed the `Server` instance which can be modified before it is activated. For example, additional files or directories could be added to the list of "watched" files for auto-reloading. @@ -84,6 +121,8 @@ class BasePlugin: """ return server + # Global events + def on_config(self, config: Config) -> Optional[Config]: """ The `config` event is the first event called on build and is run immediately @@ -395,7 +434,10 @@ class PluginCollection(OrderedDict): be modified by the event method. """ pass_item = item is not None - for method in self.events[name]: + events = self.events[name] + if events: + log.debug(f'Running {len(events)} `{name}` events') + for method in events: if pass_item: result = method(item, **kwargs) else: diff --git a/mkdocs/tests/plugin_tests.py b/mkdocs/tests/plugin_tests.py index 7b1f6866..721e82f8 100644 --- a/mkdocs/tests/plugin_tests.py +++ b/mkdocs/tests/plugin_tests.py @@ -97,6 +97,8 @@ class TestPluginCollection(unittest.TestCase): self.assertEqual( collection.events, { + 'startup': [], + 'shutdown': [], 'serve': [], 'config': [], 'pre_build': [plugin.on_pre_build], diff --git a/requirements/project-min.txt b/requirements/project-min.txt index ce88eeae..8919601d 100644 --- a/requirements/project-min.txt +++ b/requirements/project-min.txt @@ -7,6 +7,7 @@ watchdog==2.0.0 ghp-import==1.0 pyyaml_env_tag==0.1 importlib_metadata==4.3; python_version < "3.10" +typing_extensions==3.10; python_version < "3.8" packaging==20.5 mergedeep==1.3.4 babel==2.9.0 diff --git a/requirements/project.txt b/requirements/project.txt index b952b7ba..dac875a9 100644 --- a/requirements/project.txt +++ b/requirements/project.txt @@ -7,6 +7,7 @@ watchdog>=2.0.0 ghp-import>=1.0 pyyaml_env_tag>=0.1 importlib_metadata>=4.3; python_version < "3.10" +typing_extensions>=3.10; python_version < "3.8" packaging>=20.5 mergedeep>=1.3.4 babel>=2.9.0 diff --git a/setup.py b/setup.py index e9f5856c..05304b39 100755 --- a/setup.py +++ b/setup.py @@ -72,6 +72,7 @@ setup( 'ghp-import>=1.0', 'pyyaml_env_tag>=0.1', 'importlib_metadata>=4.3; python_version < "3.10"', + 'typing_extensions>=3.10; python_version < "3.8"', 'packaging>=20.5', 'mergedeep>=1.3.4' ],