diff --git a/examples/service-fastapi/app/client.py b/examples/service-fastapi/app/client.py index e31d5ebc..4ceaaf0f 100644 --- a/examples/service-fastapi/app/client.py +++ b/examples/service-fastapi/app/client.py @@ -7,5 +7,5 @@ import httpx def get_client(): base_url = os.environ["JUPYTERHUB_API_URL"] token = os.environ["JUPYTERHUB_API_TOKEN"] - headers = {"Authorization": "Bearer %s" % token} + headers = {"Authorization": f"Bearer {token}"} return httpx.AsyncClient(base_url=base_url, headers=headers) diff --git a/examples/service-whoami-flask/whoami-flask.py b/examples/service-whoami-flask/whoami-flask.py index 8e07697b..2831d8c1 100644 --- a/examples/service-whoami-flask/whoami-flask.py +++ b/examples/service-whoami-flask/whoami-flask.py @@ -38,7 +38,7 @@ def authenticated(f): else: # redirect to login url on failed auth state = auth.generate_state(next_url=request.path) - response = make_response(redirect(auth.login_url + '&state=%s' % state)) + response = make_response(redirect(auth.login_url + f'&state={state}')) response.set_cookie(auth.state_cookie_name, state) return response diff --git a/examples/spawn-form/jupyterhub_config.py b/examples/spawn-form/jupyterhub_config.py index d6858108..6abc6fed 100644 --- a/examples/spawn-form/jupyterhub_config.py +++ b/examples/spawn-form/jupyterhub_config.py @@ -11,7 +11,7 @@ c = get_config() # noqa class DemoFormSpawner(LocalProcessSpawner): def _options_form_default(self): - default_env = "YOURNAME=%s\n" % self.user.name + default_env = f"YOURNAME={self.user.name}\n" return f"""
diff --git a/jupyterhub/_version.py b/jupyterhub/_version.py index 04e6091a..e1ccb944 100644 --- a/jupyterhub/_version.py +++ b/jupyterhub/_version.py @@ -70,6 +70,4 @@ def _check_version(hub_version, singleuser_version, log): singleuser_version, ) else: - log.debug( - "jupyterhub and jupyterhub-singleuser both on version %s" % hub_version - ) + log.debug(f"jupyterhub and jupyterhub-singleuser both on version {hub_version}") diff --git a/jupyterhub/apihandlers/auth.py b/jupyterhub/apihandlers/auth.py index 4b1f3898..85eac1bf 100644 --- a/jupyterhub/apihandlers/auth.py +++ b/jupyterhub/apihandlers/auth.py @@ -46,7 +46,7 @@ class TokenAPIHandler(APIHandler): elif orm_token.service: model = self.service_model(orm_token.service) else: - self.log.warning("%s has no user or service. Deleting..." % orm_token) + self.log.warning(f"{orm_token} has no user or service. Deleting...") self.db.delete(orm_token) self.db.commit() raise web.HTTPError(404) diff --git a/jupyterhub/apihandlers/base.py b/jupyterhub/apihandlers/base.py index ed6d8ad6..a4b25932 100644 --- a/jupyterhub/apihandlers/base.py +++ b/jupyterhub/apihandlers/base.py @@ -461,9 +461,9 @@ class APIHandler(BaseHandler): name (str): name of the model, used in error messages """ if not isinstance(model, dict): - raise web.HTTPError(400, "Invalid JSON data: %r" % model) + raise web.HTTPError(400, f"Invalid JSON data: {model!r}") if not set(model).issubset(set(model_types)): - raise web.HTTPError(400, "Invalid JSON keys: %r" % model) + raise web.HTTPError(400, f"Invalid JSON keys: {model!r}") for key, value in model.items(): if not isinstance(value, model_types[key]): raise web.HTTPError( diff --git a/jupyterhub/apihandlers/groups.py b/jupyterhub/apihandlers/groups.py index 5b55b3c7..e908b2a3 100644 --- a/jupyterhub/apihandlers/groups.py +++ b/jupyterhub/apihandlers/groups.py @@ -19,7 +19,7 @@ class _GroupAPIHandler(APIHandler): username = self.authenticator.normalize_username(username) user = self.find_user(username) if user is None: - raise web.HTTPError(400, "No such user: %s" % username) + raise web.HTTPError(400, f"No such user: {username}") users.append(user.orm_user) return users @@ -87,7 +87,7 @@ class GroupListAPIHandler(_GroupAPIHandler): for name in groupnames: existing = orm.Group.find(self.db, name=name) if existing is not None: - raise web.HTTPError(409, "Group %s already exists" % name) + raise web.HTTPError(409, f"Group {name} already exists") usernames = model.get('users', []) # check that users exist @@ -124,7 +124,7 @@ class GroupAPIHandler(_GroupAPIHandler): existing = orm.Group.find(self.db, name=group_name) if existing is not None: - raise web.HTTPError(409, "Group %s already exists" % group_name) + raise web.HTTPError(409, f"Group {group_name} already exists") usernames = model.get('users', []) # check that users exist diff --git a/jupyterhub/apihandlers/hub.py b/jupyterhub/apihandlers/hub.py index 0a69f8f8..1bba64ca 100644 --- a/jupyterhub/apihandlers/hub.py +++ b/jupyterhub/apihandlers/hub.py @@ -32,14 +32,14 @@ class ShutdownAPIHandler(APIHandler): proxy = data['proxy'] if proxy not in {True, False}: raise web.HTTPError( - 400, "proxy must be true or false, got %r" % proxy + 400, f"proxy must be true or false, got {proxy!r}" ) app.cleanup_proxy = proxy if 'servers' in data: servers = data['servers'] if servers not in {True, False}: raise web.HTTPError( - 400, "servers must be true or false, got %r" % servers + 400, f"servers must be true or false, got {servers!r}" ) app.cleanup_servers = servers diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 5a7cf71a..38c3eaa7 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -161,7 +161,7 @@ class UserListAPIHandler(APIHandler): .having(func.count(orm.Server.id) == 0) ) elif state_filter: - raise web.HTTPError(400, "Unrecognized state filter: %r" % state_filter) + raise web.HTTPError(400, f"Unrecognized state filter: {state_filter!r}") # apply eager load options query = query.options( @@ -246,15 +246,15 @@ class UserListAPIHandler(APIHandler): continue user = self.find_user(name) if user is not None: - self.log.warning("User %s already exists" % name) + self.log.warning(f"User {name} already exists") else: to_create.append(name) if invalid_names: if len(invalid_names) == 1: - msg = "Invalid username: %s" % invalid_names[0] + msg = f"Invalid username: {invalid_names[0]}" else: - msg = "Invalid usernames: %s" % ', '.join(invalid_names) + msg = "Invalid usernames: {}".format(', '.join(invalid_names)) raise web.HTTPError(400, msg) if not to_create: @@ -270,7 +270,7 @@ class UserListAPIHandler(APIHandler): try: await maybe_future(self.authenticator.add_user(user)) except Exception as e: - self.log.error("Failed to create user: %s" % name, exc_info=True) + self.log.error(f"Failed to create user: {name}", exc_info=True) self.users.delete(user) raise web.HTTPError(400, f"Failed to create user {name}: {e}") else: @@ -307,7 +307,7 @@ class UserAPIHandler(APIHandler): data = self.get_json_body() user = self.find_user(user_name) if user is not None: - raise web.HTTPError(409, "User %s already exists" % user_name) + raise web.HTTPError(409, f"User {user_name} already exists") user = self.user_from_username(user_name) if data: @@ -320,10 +320,10 @@ class UserAPIHandler(APIHandler): try: await maybe_future(self.authenticator.add_user(user)) except Exception: - self.log.error("Failed to create user: %s" % user_name, exc_info=True) + self.log.error(f"Failed to create user: {user_name}", exc_info=True) # remove from registry self.users.delete(user) - raise web.HTTPError(400, "Failed to create user: %s" % user_name) + raise web.HTTPError(400, f"Failed to create user: {user_name}") self.write(json.dumps(self.user_model(user))) self.set_status(201) @@ -338,15 +338,14 @@ class UserAPIHandler(APIHandler): if user.spawner._stop_pending: raise web.HTTPError( 400, - "%s's server is in the process of stopping, please wait." % user_name, + f"{user_name}'s server is in the process of stopping, please wait.", ) if user.running: await self.stop_single_user(user) if user.spawner._stop_pending: raise web.HTTPError( 400, - "%s's server is in the process of stopping, please wait." - % user_name, + f"{user_name}'s server is in the process of stopping, please wait.", ) await maybe_future(self.authenticator.delete_user(user)) @@ -370,7 +369,9 @@ class UserAPIHandler(APIHandler): if self.find_user(data['name']): raise web.HTTPError( 400, - "User %s already exists, username must be unique" % data['name'], + "User {} already exists, username must be unique".format( + data['name'] + ), ) for key, value in data.items(): if key == 'auth_state': @@ -402,7 +403,7 @@ class UserTokenListAPIHandler(APIHandler): """Get tokens for a given user""" user = self.find_user(user_name) if not user: - raise web.HTTPError(404, "No such user: %s" % user_name) + raise web.HTTPError(404, f"No such user: {user_name}") now = utcnow(with_tz=False) api_tokens = [] @@ -624,7 +625,7 @@ class UserServerAPIHandler(APIHandler): finally: spawner._spawn_pending = False if state is None: - raise web.HTTPError(400, "%s is already running" % spawner._log_name) + raise web.HTTPError(400, f"{spawner._log_name} is already running") options = self.get_json_body() await self.spawn_single_user(user, server_name, options=options) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 10a7d5e2..acf0aebd 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -213,7 +213,7 @@ class NewToken(Application): ThreadPoolExecutor(1).submit(init_roles_and_users).result() user = orm.User.find(hub.db, self.name) if user is None: - print("No such user: %s" % self.name, file=sys.stderr) + print(f"No such user: {self.name}", file=sys.stderr) self.exit(1) token = user.new_api_token(note="command-line generated") print(token) @@ -1475,7 +1475,7 @@ class JupyterHub(Application): new = change['new'] if '://' not in new: # assume sqlite, if given as a plain filename - self.db_url = 'sqlite:///%s' % new + self.db_url = f'sqlite:///{new}' db_kwargs = Dict( help="""Include any kwargs to pass to the database connection. @@ -1778,10 +1778,10 @@ class JupyterHub(Application): [ # add trailing / to ``/user|services/:name` ( - r"%s(user|services)/([^/]+)" % self.base_url, + rf"{self.base_url}(user|services)/([^/]+)", handlers.AddSlashHandler, ), - (r"(?!%s).*" % self.hub_prefix, handlers.PrefixRedirectHandler), + (rf"(?!{self.hub_prefix}).*", handlers.PrefixRedirectHandler), (r'(.*)', handlers.Template404), ] ) @@ -1910,7 +1910,7 @@ class JupyterHub(Application): default_alt_names = ["IP:127.0.0.1", "DNS:localhost"] if self.subdomain_host: default_alt_names.append( - "DNS:%s" % urlparse(self.subdomain_host).hostname + f"DNS:{urlparse(self.subdomain_host).hostname}" ) # The signed certs used by hub-internal components try: @@ -2095,7 +2095,7 @@ class JupyterHub(Application): ck.check_available() except Exception as e: self.exit( - "auth_state is enabled, but encryption is not available: %s" % e + f"auth_state is enabled, but encryption is not available: {e}" ) # give the authenticator a chance to check its own config @@ -2114,7 +2114,7 @@ class JupyterHub(Application): self.authenticator.admin_users = set(admin_users) # force normalization for username in admin_users: if not self.authenticator.validate_username(username): - raise ValueError("username %r is not valid" % username) + raise ValueError(f"username {username!r} is not valid") new_users = [] @@ -2138,7 +2138,7 @@ class JupyterHub(Application): self.authenticator.allowed_users = set(allowed_users) # force normalization for username in allowed_users: if not self.authenticator.validate_username(username): - raise ValueError("username %r is not valid" % username) + raise ValueError(f"username {username!r} is not valid") if self.authenticator.allowed_users and self.authenticator.admin_users: # make sure admin users are in the allowed_users set, if defined, @@ -2206,7 +2206,7 @@ class JupyterHub(Application): user = orm.User.find(self.db, name=username) if user is None: if not self.authenticator.validate_username(username): - raise ValueError("Username %r is not valid" % username) + raise ValueError(f"Username {username!r} is not valid") self.log.info(f"Creating user {username} found in {hint}") user = orm.User(name=username) self.db.add(user) @@ -2317,9 +2317,7 @@ class JupyterHub(Application): old_role = orm.Role.find(self.db, name=role_name) if old_role: if not set(role_spec.get('scopes', [])).issubset(old_role.scopes): - self.log.warning( - "Role %s has obtained extra permissions" % role_name - ) + self.log.warning(f"Role {role_name} has obtained extra permissions") roles_with_new_permissions.append(role_name) # make sure we load any default roles not overridden @@ -2583,14 +2581,14 @@ class JupyterHub(Application): elif kind == 'service': Class = orm.Service else: - raise ValueError("kind must be user or service, not %r" % kind) + raise ValueError(f"kind must be user or service, not {kind!r}") db = self.db for token, name in token_dict.items(): if kind == 'user': name = self.authenticator.normalize_username(name) if not self.authenticator.validate_username(name): - raise ValueError("Token user name %r is not valid" % name) + raise ValueError(f"Token user name {name!r} is not valid") if kind == 'service': if not any(service_name == name for service_name in self._service_map): self.log.warning( @@ -2790,7 +2788,7 @@ class JupyterHub(Application): for key, value in spec.items(): trait = traits.get(key) if trait is None: - raise AttributeError("No such service field: %s" % key) + raise AttributeError(f"No such service field: {key}") setattr(service, key, value) # also set the value on the orm object # unless it's marked as not in the db @@ -2863,7 +2861,7 @@ class JupyterHub(Application): client_id=service.oauth_client_id, client_secret=service.api_token, redirect_uri=service.oauth_redirect_uri, - description="JupyterHub service %s" % service.name, + description=f"JupyterHub service {service.name}", ) service.orm.oauth_client = oauth_client # add access-scopes, derived from OAuthClient itself @@ -3456,7 +3454,7 @@ class JupyterHub(Application): answer = '' def ask(): - prompt = "Overwrite %s with default config? [y/N]" % self.config_file + prompt = f"Overwrite {self.config_file} with default config? [y/N]" try: return input(prompt).lower() or 'n' except KeyboardInterrupt: @@ -3473,7 +3471,7 @@ class JupyterHub(Application): config_text = self.generate_config_file() if isinstance(config_text, bytes): config_text = config_text.decode('utf8') - print("Writing default config to: %s" % self.config_file) + print(f"Writing default config to: {self.config_file}") with open(self.config_file, mode='w') as f: f.write(config_text) diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index a109548c..b3816b31 100644 --- a/jupyterhub/auth.py +++ b/jupyterhub/auth.py @@ -332,7 +332,7 @@ class Authenticator(LoggingConfigurable): if short_names: sorted_names = sorted(short_names) single = ''.join(sorted_names) - string_set_typo = "set('%s')" % single + string_set_typo = f"set('{single}')" self.log.warning( "Allowed set contains single-character names: %s; did you mean set([%r]) instead of %s?", sorted_names[:8], @@ -663,7 +663,7 @@ class Authenticator(LoggingConfigurable): return if isinstance(authenticated, dict): if 'name' not in authenticated: - raise ValueError("user missing a name: %r" % authenticated) + raise ValueError(f"user missing a name: {authenticated!r}") else: authenticated = {'name': authenticated} authenticated.setdefault('auth_state', None) @@ -850,7 +850,7 @@ class Authenticator(LoggingConfigurable): user (User): The User wrapper object """ if not self.validate_username(user.name): - raise ValueError("Invalid username: %s" % user.name) + raise ValueError(f"Invalid username: {user.name}") if self.allow_existing_users and not self.allow_all: self.allowed_users.add(user.name) @@ -1122,7 +1122,7 @@ class LocalAuthenticator(Authenticator): try: group = self._getgrnam(grnam) except KeyError: - self.log.error('No such group: [%s]' % grnam) + self.log.error(f'No such group: [{grnam}]') continue if group.gr_gid in user_group_gids: return True @@ -1193,7 +1193,7 @@ class LocalAuthenticator(Authenticator): uid = self.uids[name] cmd += ['--uid', '%d' % uid] except KeyError: - self.log.debug("No UID for user %s" % name) + self.log.debug(f"No UID for user {name}") cmd += [name] self.log.info("Creating user: %s", ' '.join(map(shlex.quote, cmd))) p = Popen(cmd, stdout=PIPE, stderr=STDOUT) diff --git a/jupyterhub/crypto.py b/jupyterhub/crypto.py index 854017f8..89be16d7 100644 --- a/jupyterhub/crypto.py +++ b/jupyterhub/crypto.py @@ -33,7 +33,7 @@ class CryptographyUnavailable(EncryptionUnavailable): class NoEncryptionKeys(EncryptionUnavailable): def __str__(self): - return "Encryption keys must be specified in %s env" % KEY_ENV + return f"Encryption keys must be specified in {KEY_ENV} env" def _validate_key(key): diff --git a/jupyterhub/dbutil.py b/jupyterhub/dbutil.py index 69ec87d4..452ab287 100644 --- a/jupyterhub/dbutil.py +++ b/jupyterhub/dbutil.py @@ -95,7 +95,7 @@ def backup_db_file(db_file, log=None): backup_db_file = f'{db_file}.{timestamp}.{i}' # if os.path.exists(backup_db_file): - raise OSError("backup db file already exists: %s" % backup_db_file) + raise OSError(f"backup db file already exists: {backup_db_file}") if log: log.info("Backing up %s => %s", db_file, backup_db_file) shutil.copy(db_file, backup_db_file) @@ -167,7 +167,7 @@ def main(args=None): # to subcommands choices = ['shell', 'alembic'] if not args or args[0] not in choices: - print("Select a command from: %s" % ', '.join(choices)) + print("Select a command from: {}".format(', '.join(choices))) return 1 cmd, args = args[0], args[1:] diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 69331465..7298d622 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -1512,7 +1512,7 @@ class BaseHandler(RequestHandler): # so we run it sync here, instead of making a sync version of render_template try: - html = self.render_template('%s.html' % status_code, sync=True, **ns) + html = self.render_template(f'{status_code}.html', sync=True, **ns) except TemplateNotFound: self.log.debug("Using default error template for %d", status_code) try: diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index ce26c44a..dcb6ea2b 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -236,12 +236,12 @@ class SpawnHandler(BaseHandler): if for_user != user.name: user = self.find_user(for_user) if user is None: - raise web.HTTPError(404, "No such user: %s" % for_user) + raise web.HTTPError(404, f"No such user: {for_user}") spawner = user.get_spawner(server_name, replace_failed=True) if spawner.ready: - raise web.HTTPError(400, "%s is already running" % (spawner._log_name)) + raise web.HTTPError(400, f"{spawner._log_name} is already running") elif spawner.pending: raise web.HTTPError( 400, f"{spawner._log_name} is pending {spawner.pending}" @@ -251,7 +251,7 @@ class SpawnHandler(BaseHandler): for key, byte_list in self.request.body_arguments.items(): form_options[key] = [bs.decode('utf8') for bs in byte_list] for key, byte_list in self.request.files.items(): - form_options["%s_file" % key] = byte_list + form_options[f"{key}_file"] = byte_list try: self.log.debug( "Triggering spawn with supplied form options for %s", spawner._log_name @@ -345,7 +345,7 @@ class SpawnPendingHandler(BaseHandler): if for_user != current_user.name: user = self.find_user(for_user) if user is None: - raise web.HTTPError(404, "No such user: %s" % for_user) + raise web.HTTPError(404, f"No such user: {for_user}") if server_name and server_name not in user.spawners: raise web.HTTPError(404, f"{user.name} has no such server {server_name}") @@ -642,7 +642,7 @@ class ProxyErrorHandler(BaseHandler): message_html = ' '.join( [ "Your server appears to be down.", - "Try restarting it from the hub" % hub_home, + f"Try restarting it from the hub", ] ) ns = dict( @@ -655,7 +655,7 @@ class ProxyErrorHandler(BaseHandler): self.set_header('Content-Type', 'text/html') # render the template try: - html = await self.render_template('%s.html' % status_code, **ns) + html = await self.render_template(f'{status_code}.html', **ns) except TemplateNotFound: self.log.debug("Using default error template for %d", status_code) html = await self.render_template('error.html', **ns) diff --git a/jupyterhub/oauth/provider.py b/jupyterhub/oauth/provider.py index a60b3d9c..d108a2ac 100644 --- a/jupyterhub/oauth/provider.py +++ b/jupyterhub/oauth/provider.py @@ -156,7 +156,7 @@ class JupyterHubRequestValidator(RequestValidator): self.db.query(orm.OAuthClient).filter_by(identifier=client_id).first() ) if orm_client is None: - raise ValueError("No such client: %s" % client_id) + raise ValueError(f"No such client: {client_id}") scopes = set(orm_client.allowed_scopes) if 'inherit' not in scopes: # add identify-user scope @@ -255,7 +255,7 @@ class JupyterHubRequestValidator(RequestValidator): self.db.query(orm.OAuthClient).filter_by(identifier=client_id).first() ) if orm_client is None: - raise ValueError("No such client: %s" % client_id) + raise ValueError(f"No such client: {client_id}") orm_code = orm.OAuthCode( code=code['code'], @@ -345,7 +345,7 @@ class JupyterHubRequestValidator(RequestValidator): app_log.debug("Saving bearer token %s", log_token) if request.user is None: - raise ValueError("No user for access token: %s" % request.user) + raise ValueError(f"No user for access token: {request.user}") client = ( self.db.query(orm.OAuthClient) .filter_by(identifier=request.client.client_id) diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index b690ed14..91504a4a 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -1113,7 +1113,7 @@ class APIToken(Hashed, Base): elif kind == 'service': prefix_match = prefix_match.filter(cls.service_id != None) elif kind is not None: - raise ValueError("kind must be 'user', 'service', or None, not %r" % kind) + raise ValueError(f"kind must be 'user', 'service', or None, not {kind!r}") for orm_token in prefix_match: if orm_token.match(token): if not orm_token.client_id: diff --git a/jupyterhub/proxy.py b/jupyterhub/proxy.py index 840e22a5..4b63c665 100644 --- a/jupyterhub/proxy.py +++ b/jupyterhub/proxy.py @@ -221,11 +221,11 @@ class Proxy(LoggingConfigurable): host_route = not routespec.startswith('/') if host_route and not self.host_routing: raise ValueError( - "Cannot add host-based route %r, not using host-routing" % routespec + f"Cannot add host-based route {routespec!r}, not using host-routing" ) if self.host_routing and not host_route: raise ValueError( - "Cannot add route without host %r, using host-routing" % routespec + f"Cannot add route without host {routespec!r}, using host-routing" ) # add trailing slash if not routespec.endswith('/'): @@ -613,8 +613,8 @@ class ConfigurableHTTPProxy(Proxy): # check for required token if proxy is external if not self.auth_token and not self.should_start: raise ValueError( - "%s.auth_token or CONFIGPROXY_AUTH_TOKEN env is required" - " if Proxy.should_start is False" % self.__class__.__name__ + f"{self.__class__.__name__}.auth_token or CONFIGPROXY_AUTH_TOKEN env is required" + " if Proxy.should_start is False" ) def _check_previous_process(self): @@ -758,11 +758,11 @@ class ConfigurableHTTPProxy(Proxy): ) except FileNotFoundError as e: self.log.error( - "Failed to find proxy %r\n" + f"Failed to find proxy {self.command!r}\n" "The proxy can be installed with `npm install -g configurable-http-proxy`." "To install `npm`, install nodejs which includes `npm`." "If you see an `EACCES` error or permissions error, refer to the `npm` " - "documentation on How To Prevent Permissions Errors." % self.command + "documentation on How To Prevent Permissions Errors." ) raise diff --git a/jupyterhub/scopes.py b/jupyterhub/scopes.py index 63fb86bc..d3be9a8b 100644 --- a/jupyterhub/scopes.py +++ b/jupyterhub/scopes.py @@ -1257,7 +1257,7 @@ def define_custom_scopes(scopes): The keys are the scopes, while the values are dictionaries with at least a `description` field, and optional `subscopes` field. - %s + CUSTOM_SCOPE_DESCRIPTION Examples:: define_custom_scopes( @@ -1274,7 +1274,7 @@ def define_custom_scopes(scopes): }, } ) - """ % indent(_custom_scope_description, " " * 8) + """.replace("CUSTOM_SCOPE_DESCRIPTION", indent(_custom_scope_description, " " * 8)) for scope, scope_definition in scopes.items(): if scope in scope_definitions and scope_definitions[scope] != scope_definition: raise ValueError( diff --git a/jupyterhub/services/auth.py b/jupyterhub/services/auth.py index 655018f0..be2cd647 100644 --- a/jupyterhub/services/auth.py +++ b/jupyterhub/services/auth.py @@ -613,11 +613,8 @@ class HubAuth(SingletonConfigurable): r = await AsyncHTTPClient().fetch(req, raise_error=False) except Exception as e: app_log.error("Error connecting to %s: %s", self.api_url, e) - msg = "Failed to connect to Hub API at %r." % self.api_url - msg += ( - " Is the Hub accessible at this URL (from host: %s)?" - % socket.gethostname() - ) + msg = f"Failed to connect to Hub API at {self.api_url!r}." + msg += f" Is the Hub accessible at this URL (from host: {socket.gethostname()})?" if '127.0.0.1' in self.api_url: msg += ( " Make sure to set c.JupyterHub.hub_ip to an IP accessible to" @@ -1045,7 +1042,7 @@ class HubOAuth(HubAuth): @validate('oauth_client_id', 'api_token') def _ensure_not_empty(self, proposal): if not proposal.value: - raise ValueError("%s cannot be empty." % proposal.trait.name) + raise ValueError(f"{proposal.trait.name} cannot be empty.") return proposal.value oauth_redirect_uri = Unicode( @@ -1561,7 +1558,7 @@ class HubOAuthCallbackHandler(HubOAuthenticated, RequestHandler): error = self.get_argument("error", False) if error: msg = self.get_argument("error_description", error) - raise HTTPError(400, "Error in oauth: %s" % msg) + raise HTTPError(400, f"Error in oauth: {msg}") code = self.get_argument("code", False) if not code: diff --git a/jupyterhub/services/service.py b/jupyterhub/services/service.py index fb7f6b4c..d3c75e1e 100644 --- a/jupyterhub/services/service.py +++ b/jupyterhub/services/service.py @@ -327,7 +327,7 @@ class Service(LoggingConfigurable): @default('oauth_client_id') def _default_client_id(self): - return 'service-%s' % self.name + return f'service-{self.name}' @validate("oauth_client_id") def _validate_client_id(self, proposal): @@ -419,7 +419,7 @@ class Service(LoggingConfigurable): async def start(self): """Start a managed service""" if not self.managed: - raise RuntimeError("Cannot start unmanaged service %s" % self) + raise RuntimeError(f"Cannot start unmanaged service {self}") self.log.info("Starting service %r: %r", self.name, self.command) env = {} env.update(self.environment) @@ -473,7 +473,7 @@ class Service(LoggingConfigurable): """Stop a managed service""" self.log.debug("Stopping service %s", self.name) if not self.managed: - raise RuntimeError("Cannot stop unmanaged service %s" % self) + raise RuntimeError(f"Cannot stop unmanaged service {self}") if self.spawner: if self.orm.server: self.db.delete(self.orm.server) diff --git a/jupyterhub/singleuser/mixins.py b/jupyterhub/singleuser/mixins.py index 3217e4ad..73c0bc9d 100755 --- a/jupyterhub/singleuser/mixins.py +++ b/jupyterhub/singleuser/mixins.py @@ -341,7 +341,7 @@ class SingleUserNotebookAppMixin(Configurable): # If we receive a non-absolute path, make it absolute. value = os.path.abspath(value) if not os.path.isdir(value): - raise TraitError("No such notebook dir: %r" % value) + raise TraitError(f"No such notebook dir: {value!r}") return value @default('log_level') diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index 7cc83576..8e8f320b 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -994,7 +994,7 @@ class Spawner(LoggingConfigurable): env = {} if self.env: warnings.warn( - "Spawner.env is deprecated, found %s" % self.env, DeprecationWarning + f"Spawner.env is deprecated, found {self.env}", DeprecationWarning ) env.update(self.env) @@ -1494,7 +1494,7 @@ def _try_setcwd(path): path, _ = os.path.split(path) else: return - print("Couldn't set CWD at all (%s), using temp dir" % exc, file=sys.stderr) + print(f"Couldn't set CWD at all ({exc}), using temp dir", file=sys.stderr) td = mkdtemp() os.chdir(td) @@ -1524,7 +1524,7 @@ def set_user_setuid(username, chdir=True): try: os.setgroups(gids) except Exception as e: - print('Failed to set groups %s' % e, file=sys.stderr) + print(f'Failed to set groups {e}', file=sys.stderr) os.setuid(uid) # start in the user's home dir diff --git a/jupyterhub/tests/conftest.py b/jupyterhub/tests/conftest.py index 82968d97..67366d5a 100644 --- a/jupyterhub/tests/conftest.py +++ b/jupyterhub/tests/conftest.py @@ -83,7 +83,7 @@ async def app(request, io_loop, ssl_tmpdir): try: mocked_app.stop() except Exception as e: - print("Error stopping Hub: %s" % e, file=sys.stderr) + print(f"Error stopping Hub: {e}", file=sys.stderr) request.addfinalizer(fin) await mocked_app.initialize([]) diff --git a/jupyterhub/tests/mockservice.py b/jupyterhub/tests/mockservice.py index 69db5171..f552f411 100644 --- a/jupyterhub/tests/mockservice.py +++ b/jupyterhub/tests/mockservice.py @@ -54,7 +54,7 @@ class APIHandler(web.RequestHandler): api_token = os.environ['JUPYTERHUB_API_TOKEN'] api_url = os.environ['JUPYTERHUB_API_URL'] r = requests.get( - api_url + path, headers={'Authorization': 'token %s' % api_token} + api_url + path, headers={'Authorization': f'token {api_token}'} ) r.raise_for_status() self.set_header('Content-Type', 'application/json') diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index ec0353aa..b4ed407f 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -63,7 +63,7 @@ async def test_auth_api(app): app, 'authorizations/token', api_token, - headers={'Authorization': 'token: %s' % user.cookie_id}, + headers={'Authorization': f'token: {user.cookie_id}'}, ) assert r.status_code == 403 @@ -965,7 +965,7 @@ async def test_spawn(app): status = await app_user.spawner.poll() assert status is None - assert spawner.server.base_url == ujoin(app.base_url, 'user/%s' % name) + '/' + assert spawner.server.base_url == ujoin(app.base_url, f'user/{name}') + '/' url = public_url(app, user) kwargs = {} if app.internal_ssl: @@ -1412,7 +1412,7 @@ async def test_progress_bad_slow(request, app, no_patience, slow_bad_spawn): async def progress_forever(): """progress function that yields messages forever""" for i in range(1, 10): - yield {'progress': i, 'message': 'Stage %s' % i} + yield {'progress': i, 'message': f'Stage {i}'} # wait a long time before the next event await asyncio.sleep(10) @@ -1741,7 +1741,7 @@ async def test_token_for_user(app, as_user, for_user, status): if for_user != 'missing': for_user_obj = add_user(app.db, app, name=for_user) data = {'username': for_user} - headers = {'Authorization': 'token %s' % u.new_api_token()} + headers = {'Authorization': f'token {u.new_api_token()}'} r = await api_request( app, 'users', @@ -1765,7 +1765,7 @@ async def test_token_for_user(app, as_user, for_user, status): if for_user == as_user: note = 'Requested via api' else: - note = 'Requested via api by user %s' % as_user + note = f'Requested via api by user {as_user}' assert reply['note'] == note # delete the token @@ -1836,7 +1836,7 @@ async def test_token_list(app, as_user, for_user, status): u = add_user(app.db, app, name=as_user) if for_user != 'missing': for_user_obj = add_user(app.db, app, name=for_user) - headers = {'Authorization': 'token %s' % u.new_api_token()} + headers = {'Authorization': f'token {u.new_api_token()}'} r = await api_request(app, 'users', for_user, 'tokens', headers=headers) assert r.status_code == status if status != 200: @@ -2214,7 +2214,7 @@ async def test_get_service(app, mockservice_url): r = await api_request( app, f"services/{mockservice.name}", - headers={'Authorization': 'token %s' % mockservice.api_token}, + headers={'Authorization': f'token {mockservice.api_token}'}, ) r.raise_for_status() diff --git a/jupyterhub/tests/test_db.py b/jupyterhub/tests/test_db.py index 6d696a5b..ade083ba 100644 --- a/jupyterhub/tests/test_db.py +++ b/jupyterhub/tests/test_db.py @@ -57,7 +57,7 @@ async def test_upgrade(tmpdir, hub_version): # use persistent temp env directory # to reuse across multiple runs - env_dir = os.path.join(tempfile.gettempdir(), 'test-hub-upgrade-%s' % hub_version) + env_dir = os.path.join(tempfile.gettempdir(), f'test-hub-upgrade-{hub_version}') generate_old_db(env_dir, hub_version, db_url) diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index 6a622d08..fb6de52c 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -120,7 +120,7 @@ async def test_admin_version(app): @pytest.mark.parametrize('sort', ['running', 'last_activity', 'admin', 'name']) async def test_admin_sort(app, sort): cookies = await app.login_user('admin') - r = await get_page('admin?sort=%s' % sort, app, cookies=cookies) + r = await get_page(f'admin?sort={sort}', app, cookies=cookies) r.raise_for_status() assert r.status_code == 200 @@ -170,7 +170,7 @@ async def test_spawn_redirect(app, last_failed): r.raise_for_status() print(urlparse(r.url)) path = urlparse(r.url).path - assert path == ujoin(app.base_url, '/user/%s/' % name) + assert path == ujoin(app.base_url, f'/user/{name}/') # stop server to ensure /user/name is handled by the Hub r = await api_request( @@ -181,7 +181,7 @@ async def test_spawn_redirect(app, last_failed): # test handing of trailing slash on `/user/name` r = await get_page('user/' + name, app, hub=False, cookies=cookies) path = urlparse(r.url).path - assert path == ujoin(app.base_url, 'hub/user/%s/' % name) + assert path == ujoin(app.base_url, f'hub/user/{name}/') assert r.status_code == 424 @@ -586,7 +586,7 @@ async def test_user_redirect(app, username): await asyncio.sleep(0.1) r = await async_requests.get(r.url, cookies=cookies) path = urlparse(r.url).path - assert path == ujoin(app.base_url, '/user/%s/notebooks/test.ipynb' % name) + assert path == ujoin(app.base_url, f'/user/{name}/notebooks/test.ipynb') async def test_user_redirect_hook(app, username): @@ -1240,7 +1240,7 @@ async def test_token_page(app): r.raise_for_status() body = extract_body(r) assert "API Tokens" in body, body - assert "Server at %s" % user.base_url in body, body + assert f"Server at {user.base_url}" in body, body assert "Authorized Applications" in body, body @@ -1299,7 +1299,7 @@ async def test_pre_spawn_start_exc_options_form(app): r.raise_for_status() assert FormSpawner.options_form in r.text # spawning the user server should throw the pre_spawn_start error - with pytest.raises(Exception, match="%s" % exc): + with pytest.raises(Exception, match=str(exc)): await user.spawn() diff --git a/jupyterhub/tests/test_proxy.py b/jupyterhub/tests/test_proxy.py index a91adb85..3f821c13 100644 --- a/jupyterhub/tests/test_proxy.py +++ b/jupyterhub/tests/test_proxy.py @@ -171,7 +171,7 @@ async def test_external_proxy(request): async def test_check_routes(app, username, disable_check_routes): proxy = app.proxy test_user = add_user(app.db, app, name=username) - r = await api_request(app, 'users/%s/server' % username, method='post') + r = await api_request(app, f'users/{username}/server', method='post') r.raise_for_status() # check a valid route exists for user diff --git a/jupyterhub/tests/test_roles.py b/jupyterhub/tests/test_roles.py index a112086a..88f0e562 100644 --- a/jupyterhub/tests/test_roles.py +++ b/jupyterhub/tests/test_roles.py @@ -956,7 +956,7 @@ async def test_user_group_roles(app, create_temp_role): # jack's API token token = user.new_api_token() - headers = {'Authorization': 'token %s' % token} + headers = {'Authorization': f'token {token}'} r = await api_request(app, f'users/{user.name}', method='get', headers=headers) assert r.status_code == 200 r.raise_for_status() @@ -968,7 +968,7 @@ async def test_user_group_roles(app, create_temp_role): assert len(reply['roles']) == 1 assert group_role.name not in reply['roles'] - headers = {'Authorization': 'token %s' % token} + headers = {'Authorization': f'token {token}'} r = await api_request(app, 'groups', method='get', headers=headers) assert r.status_code == 200 r.raise_for_status() @@ -978,7 +978,7 @@ async def test_user_group_roles(app, create_temp_role): assert len(reply) == 1 assert reply[0]['name'] == 'A' - headers = {'Authorization': 'token %s' % token} + headers = {'Authorization': f'token {token}'} r = await api_request(app, f'users/{user.name}', method='get', headers=headers) assert r.status_code == 200 r.raise_for_status() diff --git a/jupyterhub/tests/test_scopes.py b/jupyterhub/tests/test_scopes.py index ba2bfae0..bbf58fce 100644 --- a/jupyterhub/tests/test_scopes.py +++ b/jupyterhub/tests/test_scopes.py @@ -289,7 +289,7 @@ async def test_exceeding_user_permissions( orm_api_token = orm.APIToken.find(app.db, token=api_token) # store scopes user does not have orm_api_token.scopes = list(orm_api_token.scopes) + ['list:users', 'read:users'] - headers = {'Authorization': 'token %s' % api_token} + headers = {'Authorization': f'token {api_token}'} r = await api_request(app, 'users', headers=headers) assert r.status_code == 200 keys = {key for user in r.json() for key in user.keys()} @@ -307,7 +307,7 @@ async def test_user_service_separation(app, mockservice_url, create_temp_role): roles.update_roles(app.db, mockservice_url.orm, roles=['reader_role']) user.roles.remove(orm.Role.find(app.db, name='user')) api_token = user.new_api_token() - headers = {'Authorization': 'token %s' % api_token} + headers = {'Authorization': f'token {api_token}'} r = await api_request(app, 'users', headers=headers) assert r.status_code == 200 keys = {key for user in r.json() for key in user.keys()} @@ -551,7 +551,7 @@ async def test_server_state_access( ) service = create_service_with_scopes("read:users:name!user=bianca", *scopes) api_token = service.new_api_token() - headers = {'Authorization': 'token %s' % api_token} + headers = {'Authorization': f'token {api_token}'} # can I get the user model? r = await api_request(app, 'users', user.name, headers=headers) diff --git a/jupyterhub/tests/test_services_auth.py b/jupyterhub/tests/test_services_auth.py index e25d81aa..20dbdf57 100644 --- a/jupyterhub/tests/test_services_auth.py +++ b/jupyterhub/tests/test_services_auth.py @@ -88,7 +88,7 @@ async def test_hubauth_token(app, mockservice_url, create_user_with_scopes): # token in ?token parameter is not allowed by default r = await async_requests.get( - public_url(app, mockservice_url) + '/whoami/?token=%s' % token, + public_url(app, mockservice_url) + f'/whoami/?token={token}', allow_redirects=False, ) assert r.status_code == 403 @@ -150,7 +150,7 @@ async def test_hubauth_service_token(request, app, mockservice_url, scopes, allo # token in Authorization header r = await async_requests.get( public_url(app, mockservice_url) + 'whoami/', - headers={'Authorization': 'token %s' % token}, + headers={'Authorization': f'token {token}'}, allow_redirects=False, ) service_model = { @@ -170,7 +170,7 @@ async def test_hubauth_service_token(request, app, mockservice_url, scopes, allo # token in ?token parameter is not allowed by default r = await async_requests.get( - public_url(app, mockservice_url) + 'whoami/?token=%s' % token, + public_url(app, mockservice_url) + f'whoami/?token={token}', allow_redirects=False, ) assert r.status_code == 403 @@ -303,7 +303,7 @@ async def test_oauth_service_roles( # we should be looking at the oauth confirmation page assert urlparse(r.url).path == app.base_url + 'hub/api/oauth2/authorize' # verify oauth state cookie was set at some point - assert set(r.history[0].cookies.keys()) == {'service-%s-oauth-state' % service.name} + assert set(r.history[0].cookies.keys()) == {f'service-{service.name}-oauth-state'} page = BeautifulSoup(r.text, "html.parser") scope_inputs = page.find_all("input", {"name": "scopes"}) @@ -318,9 +318,9 @@ async def test_oauth_service_roles( r.raise_for_status() assert r.url == url # verify oauth cookie is set - assert 'service-%s' % service.name in set(s.cookies.keys()) + assert f'service-{service.name}' in set(s.cookies.keys()) # verify oauth state cookie has been consumed - assert 'service-%s-oauth-state' % service.name not in set(s.cookies.keys()) + assert f'service-{service.name}-oauth-state' not in set(s.cookies.keys()) # second request should be authenticated, which means no redirects r = await s.get(url, allow_redirects=False) @@ -402,16 +402,16 @@ async def test_oauth_access_scopes( # we should be looking at the oauth confirmation page assert urlparse(r.url).path == app.base_url + 'hub/api/oauth2/authorize' # verify oauth state cookie was set at some point - assert set(r.history[0].cookies.keys()) == {'service-%s-oauth-state' % service.name} + assert set(r.history[0].cookies.keys()) == {f'service-{service.name}-oauth-state'} # submit the oauth form to complete authorization r = await s.post(r.url, data={"_xsrf": s.cookies["_xsrf"]}) r.raise_for_status() assert r.url == url # verify oauth cookie is set - assert 'service-%s' % service.name in set(s.cookies.keys()) + assert f'service-{service.name}' in set(s.cookies.keys()) # verify oauth state cookie has been consumed - assert 'service-%s-oauth-state' % service.name not in set(s.cookies.keys()) + assert f'service-{service.name}-oauth-state' not in set(s.cookies.keys()) # second request should be authenticated, which means no redirects r = await s.get(url, allow_redirects=False) @@ -499,8 +499,8 @@ async def test_oauth_cookie_collision( name = 'mypha' create_user_with_scopes("access:services", name=name) s.cookies = await app.login_user(name) - state_cookie_name = 'service-%s-oauth-state' % service.name - service_cookie_name = 'service-%s' % service.name + state_cookie_name = f'service-{service.name}-oauth-state' + service_cookie_name = f'service-{service.name}' url_1 = url + "?oauth_test=1" oauth_1 = await s.get(url_1) assert state_cookie_name in s.cookies @@ -582,7 +582,7 @@ async def test_oauth_logout(app, mockservice_url, create_user_with_scopes): 4. cache hit """ service = mockservice_url - service_cookie_name = 'service-%s' % service.name + service_cookie_name = f'service-{service.name}' url = url_path_join(public_url(app, mockservice_url), 'owhoami/?foo=bar') # first request is only going to set login cookie s = AsyncSession() diff --git a/jupyterhub/tests/test_spawner.py b/jupyterhub/tests/test_spawner.py index b11df693..6155864f 100644 --- a/jupyterhub/tests/test_spawner.py +++ b/jupyterhub/tests/test_spawner.py @@ -221,8 +221,8 @@ def test_string_formatting(db): name = s.user.name assert s.notebook_dir == 'user/{username}/' assert s.default_url == '/base/{username}' - assert s.format_string(s.notebook_dir) == 'user/%s/' % name - assert s.format_string(s.default_url) == '/base/%s' % name + assert s.format_string(s.notebook_dir) == f'user/{name}/' + assert s.format_string(s.default_url) == f'/base/{name}' async def test_popen_kwargs(db): @@ -496,7 +496,7 @@ async def test_hub_connect_url(db): assert env["JUPYTERHUB_API_URL"] == "https://example.com/api" assert ( env["JUPYTERHUB_ACTIVITY_URL"] - == "https://example.com/api/users/%s/activity" % name + == f"https://example.com/api/users/{name}/activity" ) diff --git a/jupyterhub/tests/utils.py b/jupyterhub/tests/utils.py index 143b2a9a..3cfe8df9 100644 --- a/jupyterhub/tests/utils.py +++ b/jupyterhub/tests/utils.py @@ -149,7 +149,7 @@ def auth_header(db, name): if user is None: raise KeyError(f"No such user: {name}") token = user.new_api_token() - return {'Authorization': 'token %s' % token} + return {'Authorization': f'token {token}'} @check_db_locks @@ -198,7 +198,7 @@ async def api_request( def get_page(path, app, hub=True, **kw): if "://" in path: raise ValueError( - "Not a hub page path: %r. Did you mean async_requests.get?" % path + f"Not a hub page path: {path!r}. Did you mean async_requests.get?" ) if hub: prefix = app.hub.base_url diff --git a/jupyterhub/user.py b/jupyterhub/user.py index da4f6edd..c86d1089 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -125,7 +125,7 @@ class UserDict(dict): elif isinstance(key, str): orm_user = self.db.query(orm.User).filter(orm.User.name == key).first() if orm_user is None: - raise KeyError("No such user: %s" % key) + raise KeyError(f"No such user: {key}") else: key = orm_user.id if isinstance(key, orm.User): @@ -142,7 +142,7 @@ class UserDict(dict): if id not in self: orm_user = self.db.query(orm.User).filter(orm.User.id == id).first() if orm_user is None: - raise KeyError("No such user: %s" % id) + raise KeyError(f"No such user: {id}") user = self.add(orm_user) else: user = super().__getitem__(id) @@ -505,7 +505,7 @@ class User: # use fully quoted name for client_id because it will be used in cookie-name # self.escaped_name may contain @ which is legal in URLs but not cookie keys - client_id = 'jupyterhub-user-%s' % quote(self.name) + client_id = f'jupyterhub-user-{quote(self.name)}' if server_name: client_id = f'{client_id}-{quote(server_name)}' @@ -790,7 +790,7 @@ class User: orm_server = orm.Server(base_url=base_url) db.add(orm_server) - note = "Server at %s" % base_url + note = f"Server at {base_url}" db.commit() spawner = self.get_spawner(server_name, replace_failed=True) @@ -962,7 +962,7 @@ class User: ) self.db.delete(found) self.db.commit() - raise ValueError("Invalid token for %s!" % self.name) + raise ValueError(f"Invalid token for {self.name}!") else: # Spawner.api_token has changed, but isn't in the db. # What happened? Maybe something unclean in a resumed container. @@ -975,7 +975,7 @@ class User: self.new_api_token( spawner.api_token, generated=False, - note="retrieved from spawner %s" % server_name, + note=f"retrieved from spawner {server_name}", scopes=resolved_scopes, ) # update OAuth client secret with updated API token diff --git a/jupyterhub/utils.py b/jupyterhub/utils.py index 694b1088..2eb38c0d 100644 --- a/jupyterhub/utils.py +++ b/jupyterhub/utils.py @@ -504,7 +504,7 @@ def print_ps_info(file=sys.stderr): if cpu >= 10: cpu_s = "%i" % cpu else: - cpu_s = "%.1f" % cpu + cpu_s = f"{cpu:.1f}" # format memory (only resident set) rss = p.memory_info().rss @@ -562,7 +562,7 @@ def print_stacks(file=sys.stderr): print("Active threads: %i" % threading.active_count(), file=file) for thread in threading.enumerate(): - print("Thread %s:" % thread.name, end='', file=file) + print(f"Thread {thread.name}:", end='', file=file) frame = sys._current_frames()[thread.ident] stack = traceback.extract_stack(frame) if thread is threading.current_thread(): diff --git a/setup.py b/setup.py index 3adbf2f4..611b8225 100755 --- a/setup.py +++ b/setup.py @@ -157,7 +157,7 @@ class CSS(BaseCommand): try: check_call(args, cwd=here, shell=shell) except OSError as e: - print("Failed to build css: %s" % e, file=sys.stderr) + print(f"Failed to build css: {e}", file=sys.stderr) print("You can install js dependencies with `npm install`", file=sys.stderr) raise # update data-files in case this created new files