commands: allow extension commands with arbitrary names
The restriction that extension commands have names which are valid Python identifiers is arbitrary and unnecessarily restrictive. For example, it should be possible to have a command with a dash (-) in its name to separate words, like "submit-pr". This is currently not possible. Resolve that by generating module names from an infinite iterator of fresh per-file values instead of relying on the command name. We don't actually need the name! Just the canonical path of the file which defines it is important to avoid double imports. Signed-off-by: Marti Bolivar <marti@foundries.io>
This commit is contained in:
parent
71c5e7833d
commit
a1c5f5087d
|
@ -6,7 +6,7 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
import importlib
|
import importlib
|
||||||
from keyword import iskeyword
|
import itertools
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
@ -230,43 +230,25 @@ def _ext_specs_from_desc(project, commands_desc):
|
||||||
return thunks
|
return thunks
|
||||||
|
|
||||||
|
|
||||||
def _commands_module_from_file(command_name, file):
|
def _commands_module_from_file(file):
|
||||||
# Python magic for importing a module containing west extension
|
# Python magic for importing a module containing west extension
|
||||||
# commands. To avoid polluting the sys.modules key space, we put
|
# commands. To avoid polluting the sys.modules key space, we put
|
||||||
# these modules in an (otherwise unpopulated) west.commands.ext
|
# these modules in an (otherwise unpopulated) west.commands.ext
|
||||||
# package.
|
# package.
|
||||||
#
|
#
|
||||||
# The file is imported as a module named
|
# The file is imported as a module named
|
||||||
# west.commands.ext.<command_name>, so the name must be a
|
# west.commands.ext.A_FRESH_IDENTIFIER. This module object is
|
||||||
# non-keyword identifier. This module object is returned from a
|
# returned from a cache if the same file is ever imported again,
|
||||||
# cache if the same file is ever imported again, to avoid a double
|
# to avoid a double import in case the file maintains module-level
|
||||||
# import in case the file maintains module-level state or defines
|
# state or defines multiple commands.
|
||||||
# multiple commands.
|
|
||||||
#
|
|
||||||
# (This shouldn't be a problem for west itself since main.py
|
|
||||||
# executes at most one WestCommand -- and creates at most one
|
|
||||||
# external command -- per process, but better safe than sorry in
|
|
||||||
# case this code gets called from elsewhere.)
|
|
||||||
#
|
|
||||||
# If we ever try to re-use a command_name, a ValueError() is
|
|
||||||
# raised; this prevents the import machinery from returning a
|
|
||||||
# previously cached module from sys.modules for an unrelated file,
|
|
||||||
# and is a decent sanity check against two providers of the same
|
|
||||||
# command.
|
|
||||||
global _EXT_MODULES_CACHE
|
global _EXT_MODULES_CACHE
|
||||||
mod_name = 'west.commands.ext.{}'.format(command_name)
|
global _EXT_MODULES_NAME_IT
|
||||||
|
|
||||||
if not command_name.isidentifier() or iskeyword(command_name):
|
|
||||||
raise ValueError('bad command name "{}"'.format(command_name))
|
|
||||||
elif mod_name in sys.modules:
|
|
||||||
raise ValueError('command {} is already defined'.format(
|
|
||||||
mod_name.rsplit('.', 1)[-1]))
|
|
||||||
|
|
||||||
file = os.path.normcase(os.path.realpath(file))
|
file = os.path.normcase(os.path.realpath(file))
|
||||||
|
|
||||||
if file in _EXT_MODULES_CACHE:
|
if file in _EXT_MODULES_CACHE:
|
||||||
return _EXT_MODULES_CACHE[file]
|
return _EXT_MODULES_CACHE[file]
|
||||||
|
|
||||||
|
mod_name = next(_EXT_MODULES_NAME_IT)
|
||||||
# The Python 3.4 way to import a module given its file got deprecated
|
# The Python 3.4 way to import a module given its file got deprecated
|
||||||
# later on, but we still need to support 3.4. If that requirement ever
|
# later on, but we still need to support 3.4. If that requirement ever
|
||||||
# gets dropped, this code can be simplified.
|
# gets dropped, this code can be simplified.
|
||||||
|
@ -290,8 +272,12 @@ def _commands_module_from_file(command_name, file):
|
||||||
_EXT_SCHEMA_PATH = os.path.join(os.path.dirname(__file__),
|
_EXT_SCHEMA_PATH = os.path.join(os.path.dirname(__file__),
|
||||||
'west-commands-schema.yml')
|
'west-commands-schema.yml')
|
||||||
|
|
||||||
|
# Cache which maps files implementing extension commands their
|
||||||
|
# imported modules.
|
||||||
_EXT_MODULES_CACHE = {}
|
_EXT_MODULES_CACHE = {}
|
||||||
|
# Infinite iterator of "fresh" extension command module names.
|
||||||
|
_EXT_MODULES_NAME_IT = ('west.commands.ext.cmd_{}'.format(i)
|
||||||
|
for i in itertools.count(1))
|
||||||
|
|
||||||
|
|
||||||
class _ExtFactory:
|
class _ExtFactory:
|
||||||
|
@ -310,9 +296,7 @@ class _ExtFactory:
|
||||||
# Load the module containing the command. Convert only
|
# Load the module containing the command. Convert only
|
||||||
# expected exceptions to BadExternalCommand.
|
# expected exceptions to BadExternalCommand.
|
||||||
try:
|
try:
|
||||||
mod = _commands_module_from_file(self.name, self.py_file)
|
mod = _commands_module_from_file(self.py_file)
|
||||||
except ValueError as ve:
|
|
||||||
raise BadExternalCommand from ve
|
|
||||||
except ImportError as ie:
|
except ImportError as ie:
|
||||||
raise BadExternalCommand from ie
|
raise BadExternalCommand from ie
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue