diff options
| author | Chris Hofstaedtler <[email protected]> | 2024-12-29 02:16:44 +0100 |
|---|---|---|
| committer | git-ubuntu importer <[email protected]> | 2024-12-29 04:35:01 +0000 |
| commit | 423c2858a1c5d19d976e9fa5ea3a77275c2e8d9c (patch) | |
| tree | 7a9335f42d7d5e9a845996ce704364c49f3ad364 | |
| parent | b5672701f4fd6080aa0eacb988df2a277612300b (diff) | |
1:2.0.0-1 (patches unapplied)import/1%2.0.0-1ubuntu/plucky-proposedubuntu/plucky-develubuntu/plucky
Imported using git-ubuntu import.
Notes
Notes:
* Team upload
* Use PyPI as source for upstream tarballs
* New upstream version 2.0.0 (Closes: #1057841, #1087405)
* Drop now unnecessary patches
| -rw-r--r-- | .github/workflows/pre-commit.yml | 14 | ||||
| -rw-r--r-- | .github/workflows/publish.yml | 40 | ||||
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | .pre-commit-config.yaml | 23 | ||||
| -rw-r--r-- | COPYING | 1 | ||||
| -rw-r--r-- | Makefile | 49 | ||||
| -rw-r--r-- | PKG-INFO | 55 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | configshell/__init__.py | 17 | ||||
| -rw-r--r-- | configshell/console.py | 47 | ||||
| -rw-r--r-- | configshell/log.py | 16 | ||||
| -rw-r--r-- | configshell/node.py | 439 | ||||
| -rw-r--r-- | configshell/prefs.py | 35 | ||||
| -rw-r--r-- | configshell/shell.py | 240 | ||||
| l--------- | configshell_fb | 1 | ||||
| -rw-r--r-- | configshell_fb.py | 12 | ||||
| -rw-r--r-- | debian/changelog | 9 | ||||
| -rw-r--r-- | debian/control | 30 | ||||
| -rw-r--r-- | debian/patches/replace-getargspec-with-getfullargspec.patch | 31 | ||||
| -rw-r--r-- | debian/patches/replace-more-occurrences-of-getargspec-with-getfullargspec.patch | 70 | ||||
| -rw-r--r-- | debian/patches/series | 2 | ||||
| -rw-r--r-- | debian/watch | 3 | ||||
| -rwxr-xr-x | examples/myshell | 6 | ||||
| -rw-r--r-- | pyproject.toml | 100 | ||||
| -rwxr-xr-x | setup.py | 54 |
25 files changed, 647 insertions, 652 deletions
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..7f579af --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,14 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [master] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: pre-commit/[email protected] diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..7235fce --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,40 @@ +name: pypi-release + +on: + release: + types: + - published + workflow_dispatch: + inputs: + ref: + description: 'Branch, tag or SHA to checkout' + required: true + default: 'master' + +jobs: + pypi-publish: + runs-on: ubuntu-latest + + permissions: + id-token: write # Needed for trusted publishing + + environment: + name: pypi + url: https://pypi.org/p/configshell-fb + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.ref || github.ref }} + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Build a binary wheel and a source tarball + run: | + python -m pip install hatch + hatch build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 @@ -22,3 +22,6 @@ debian/tmp/ *.spec *.pyc rtslib-* +venv/ +.idea +*egg-info/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..209c747 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,23 @@ +repos: + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.6.4 + hooks: + - id: ruff + args: [--fix] + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-case-conflict + - id: check-ast + - id: check-docstring-first + - id: check-case-conflict + - id: check-merge-conflict + - id: check-builtin-literals + - id: check-docstring-first + - id: check-merge-conflict + - id: check-toml + - id: debug-statements + - id: end-of-file-fixer + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] @@ -173,4 +173,3 @@ defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - diff --git a/Makefile b/Makefile deleted file mode 100644 index a91fdc8..0000000 --- a/Makefile +++ /dev/null @@ -1,49 +0,0 @@ -PKGNAME = configshell-fb -NAME = configshell -GIT_BRANCH = $$(git branch | grep \* | tr -d \*) -VERSION = $$(basename $$(git describe --tags | tr - . | sed 's/^v//')) - -all: - @echo "Usage:" - @echo - @echo " make release - Generates the release tarball." - @echo - @echo " make clean - Cleanup the local repository build files." - @echo " make cleanall - Also remove dist/*" - -clean: - @rm -fv ${NAME}/*.pyc ${NAME}/*.html - @rm -frv doc - @rm -frv ${NAME}.egg-info MANIFEST build - @rm -frv results - @rm -frv ${PKGNAME}-* - @echo "Finished cleanup." - -cleanall: clean - @rm -frv dist - -release: build/release-stamp -build/release-stamp: - @mkdir -p build - @echo "Exporting the repository files..." - @git archive ${GIT_BRANCH} --prefix ${PKGNAME}-${VERSION}/ \ - | (cd build; tar xfp -) - @echo "Cleaning up the target tree..." - @rm -f build/${PKGNAME}-${VERSION}/Makefile - @rm -f build/${PKGNAME}-${VERSION}/.gitignore - @echo "Fixing version string..." - @sed -i "s/__version__ = .*/__version__ = '${VERSION}'/g" \ - build/${PKGNAME}-${VERSION}/${NAME}/__init__.py - @find build/${PKGNAME}-${VERSION}/ -exec \ - touch -t $$(date -d @$$(git show -s --format="format:%at") \ - +"%Y%m%d%H%M.%S") {} \; - @mkdir -p dist - @cd build; tar -c --owner=0 --group=0 --numeric-owner \ - --format=gnu -b20 --quoting-style=escape \ - -f ../dist/${PKGNAME}-${VERSION}.tar \ - $$(find ${PKGNAME}-${VERSION} -type f | sort)\ - $$(find ${PKGNAME}-${VERSION} -type l | sort) - @gzip -6 -n dist/${PKGNAME}-${VERSION}.tar - @echo "Generated release tarball:" - @echo " $$(ls dist/${PKGNAME}-${VERSION}.tar.gz)" - @touch build/release-stamp diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..17ba48a --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,55 @@ +Metadata-Version: 2.3 +Name: configshell-fb +Version: 2.0.0 +Summary: A framework to implement simple but nice CLIs. +Project-URL: Homepage, http://github.com/open-iscsi/configshell-fb +Author-email: Andy Grover <[email protected]>, Jerome Martin <[email protected]> +Maintainer-email: Maurizio Lombardi <[email protected]> +License-Expression: Apache-2.0 +License-File: COPYING +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.12 +Requires-Python: >=3.9 +Requires-Dist: pyparsing<4,>=2.4.7 +Description-Content-Type: text/markdown + +configshell-fb +============== + +A Python library for building configuration shells +-------------------------------------------------- +configshell-fb is a Python library that provides a framework +for building simple but nice CLI-based applications. + +configshell-fb development +-------------------------- +configshell-fb is licensed under the Apache 2.0 license. Contributions are welcome. + +Since configshell-fb is used most often with targetcli-fb, the +targetcli-fb mailing should be used for configshell-fb discussion. + + * Mailing list: [targetcli-fb-devel](https://lists.fedorahosted.org/mailman/listinfo/targetcli-fb-devel) + * Source repo: [GitHub](https://github.com/open-iscsi/configshell-fb) + * Bugs: [GitHub](https://github.com/open-iscsi/configshell-fb/issues) or [Trac](https://fedorahosted.org/targetcli-fb/) + * Tarballs: [fedorahosted](https://fedorahosted.org/releases/t/a/targetcli-fb/) + +Packages +-------- +configshell-fb is packaged for a number of Linux distributions +including RHEL, +[Fedora](https://apps.fedoraproject.org/packages/python-configshell), +openSUSE, Arch Linux, +[Gentoo](https://packages.gentoo.org/packages/dev-python/configshell-fb), and +[Debian](https://tracker.debian.org/pkg/python-configshell-fb). + +"fb" -- "free branch" +--------------------- + +configshell-fb is a fork of the "configshell" code written by +RisingTide Systems. The "-fb" differentiates between the original and +this version. Please ensure to use either all "fb" versions of the +targetcli components -- targetcli, rtslib, and configshell, or stick +with all non-fb versions, since they are no longer strictly +compatible. @@ -6,8 +6,6 @@ A Python library for building configuration shells configshell-fb is a Python library that provides a framework for building simple but nice CLI-based applications. -This runs with Python 2 and 2to3 is run by setup.py to run on Python 3. - configshell-fb development -------------------------- configshell-fb is licensed under the Apache 2.0 license. Contributions are welcome. diff --git a/configshell/__init__.py b/configshell/__init__.py index 26c7909..9de7a25 100644 --- a/configshell/__init__.py +++ b/configshell/__init__.py @@ -15,18 +15,17 @@ License for the specific language governing permissions and limitations under the License. ''' -if __name__ == 'configshell-fb': - from warnings import warn - warn("'configshell' package name for configshell-fb is deprecated, please" - + " instead import 'configshell_fb'", UserWarning, stacklevel=2) - from .console import Console from .log import Log from .node import ConfigNode, ExecutionError from .prefs import Prefs from .shell import ConfigShell -__version__ = '1.1.28' -__url__ = 'http://github.com/open-iscsi/configshell-fb' -__description__ = 'A framework to implement simple but nice CLIs.' -__license__ = 'Apache 2.0' +__all__ = [ + 'Console', + 'Log', + 'ConfigNode', + 'ExecutionError', + 'Prefs', + 'ConfigShell', +] diff --git a/configshell/console.py b/configshell/console.py index 8f72fa0..de99a76 100644 --- a/configshell/console.py +++ b/configshell/console.py @@ -15,18 +15,18 @@ License for the specific language governing permissions and limitations under the License. ''' -from fcntl import ioctl import re -import six import struct import sys -from termios import TIOCGWINSZ, TCSADRAIN, tcsetattr, tcgetattr import textwrap import tty +from fcntl import ioctl +from termios import TCSADRAIN, TIOCGWINSZ, tcgetattr, tcsetattr from .prefs import Prefs -class Console(object): + +class Console: ''' Implements various utility methods providing a console UI support toolkit, most notably an epytext-to-console text renderer using ANSI escape @@ -36,7 +36,7 @@ class Console(object): _escape = '\033[' _ansi_format = _escape + '%dm%s' _ansi_reset = _escape + '0m' - _re_ansi_seq = re.compile('(\033\[..?m)') + _re_ansi_seq = re.compile('(\033\\[..?m)') _ansi_styles = {'bold': 1, 'underline': 4, @@ -89,8 +89,8 @@ class Console(object): tcsetattr(self._stdin, TCSADRAIN, attributes) if reply_terminator is not None: reply = reply[:-len(reply_terminator)] - reply = reply.replace(self._escape, '').split(';') - return reply + return reply.replace(self._escape, '').split(';') + return None def get_width(self): ''' @@ -101,11 +101,10 @@ class Console(object): winsize = struct.pack("HHHH", 0, 0, 0, 0) winsize = ioctl(self._stdout.fileno(), TIOCGWINSZ, winsize) width = struct.unpack("HHHH", winsize)[1] - except IOError: + except OSError: width = self._max_width else: - if width > self._max_width: - width = self._max_width + width = min(width, self._max_width) return width @@ -167,7 +166,7 @@ class Console(object): if text[-index] == '\n': clean_text = text[:-index] if index != 1: - clean_text += text[-index+1:] + clean_text += text[-index + 1:] break else: clean_text = text @@ -195,9 +194,7 @@ class Console(object): break text = text[i:] text = textwrap.dedent(text) - text = '\n' * i + text - - return text + return '\n' * i + text def render_text(self, text, fgcolor=None, bgcolor=None, styles=None, open_end=False, todefault=False): @@ -219,9 +216,8 @@ class Console(object): @type todefault: bool ''' if self.prefs['color_mode'] and self._stdout.isatty(): - if fgcolor is None: - if self.prefs['color_default']: - fgcolor = self.prefs['color_default'] + if fgcolor is None and self.prefs['color_default']: + fgcolor = self.prefs['color_default'] if fgcolor is not None: text = self._ansi_format % (self._ansi_fgcolors[fgcolor], text) if bgcolor is not None: @@ -266,21 +262,20 @@ class Console(object): ''' right = self.get_width() if splitchars: - chunks = re.split(r'( +|\n|[^ \n%s]*[%s])' % - (re.escape(splitchars), re.escape(splitchars)), + chunks = re.split(rf'( +|\n|[^ \n{re.escape(splitchars)}]*[{re.escape(splitchars)}])', text.expandtabs()) else: chunks = re.split(r'( +|\n)', text.expandtabs()) - result = [' '*(indent-startindex)] + result = [' ' * (indent - startindex)] charindex = max(indent, startindex) current_style = '' - for chunknum, chunk in enumerate(chunks): - chunk_groups = re.split(self._re_ansi_seq, chunk) + for chunk in chunks: + chunk_groups = self._re_ansi_seq.split(chunk) chunk_text = '' next_style = current_style for group in chunk_groups: - if re.match(self._re_ansi_seq, group) is None: + if self._re_ansi_seq.match(group) is None: chunk_text += group else: next_style += group @@ -290,9 +285,9 @@ class Console(object): or chunk == '\n': result[-1] = result[-1].rstrip() result.append(self.render_text( - '\n' + ' '*indent + current_style, open_end=True)) + '\n' + ' ' * indent + current_style, open_end=True)) charindex = indent - if chunk[:1] not in ('\n', ' '): + if chunk[:1] not in {'\n', ' '}: result.append(chunk) charindex += chunk_len else: @@ -301,4 +296,4 @@ class Console(object): current_style = next_style.split(self._ansi_reset)[-1] - return ''.join(result).rstrip()+'\n' + return ''.join(result).rstrip() + '\n' diff --git a/configshell/log.py b/configshell/log.py index 003d862..fefd6bd 100644 --- a/configshell/log.py +++ b/configshell/log.py @@ -16,14 +16,15 @@ under the License. ''' import inspect -import os import time import traceback +from pathlib import Path from .console import Console from .prefs import Prefs -class Log(object): + +class Log: ''' Implements a file and console logger using python's logging facility. Log levels are, in raising criticality: @@ -86,12 +87,7 @@ class Log(object): date_fields[3], date_fields[4], date_fields[5]) if self.prefs['logfile']: - path = os.path.expanduser(self.prefs['logfile']) - handle = open(path, 'a') - try: - handle.write("[%s] %s %s\n" % (level, date, msg)) - finally: - handle.close() + Path(self.prefs['logfile']).write_text(f"[{level}] {date} {msg}\n") def _log(self, level, msg): ''' @@ -110,7 +106,7 @@ class Log(object): if self.prefs["color_mode"]: msg = self.con.render_text(msg, self.colors[level]) else: - msg = "%s: %s" % (level.capitalize(), msg) + msg = f"{level.capitalize()}: {msg}" error = False if self.levels.index(level) <= self.levels.index('error'): error = True @@ -136,7 +132,7 @@ class Log(object): ''' trace = traceback.format_exc().rstrip() if msg: - trace += '\n%s' % msg + trace += f'\n{msg}' self._log('error', trace) def verbose(self, msg): diff --git a/configshell/node.py b/configshell/node.py index 6af8642..fdf4de2 100644 --- a/configshell/node.py +++ b/configshell/node.py @@ -14,15 +14,16 @@ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' - +import curses import inspect import re -import six + class ExecutionError(Exception): pass -class ConfigNode(object): + +class ConfigNode: ''' The ConfigNode class defines a common skeleton to be used by specific implementation. It is "purely virtual" (sorry for using non-pythonic @@ -83,26 +84,24 @@ class ConfigNode(object): @type shell: ConfigShell ''' self._name = name - self._children = set([]) + self._children = set() if parent is None: if shell is None: raise ValueError("A root ConfigNode must have a shell.") - else: - self._parent = None - self._shell = shell - shell.attach_root_node(self) + + self._parent = None + self._shell = shell + shell.attach_root_node(self) + elif shell is None: + self._parent = parent + self._shell = None else: - if shell is None: - self._parent = parent - self._shell = None - else: - raise ValueError("A non-root ConfigNode can't have a shell.") + raise ValueError("A non-root ConfigNode can't have a shell.") if self._parent is not None: for sibling in self._parent._children: if sibling.name == name: - raise ValueError("Name '%s' already used by a sibling." - % self._name) + raise ValueError(f"Name '{self._name}' already used by a sibling.") self._parent._children.add(self) self._configuration_groups = {} @@ -190,8 +189,7 @@ class ConfigNode(object): try: value = int(value) except ValueError: - raise ValueError("Syntax error, '%s' is not a %s." - % (value, syntax)) + raise ValueError(f"Syntax error, '{value}' is not a {syntax}.") else: return value @@ -230,8 +228,7 @@ class ConfigNode(object): try: value = str(value) except ValueError: - raise ValueError("Syntax error, '%s' is not a %s." - % (value, syntax)) + raise ValueError(f"Syntax error, '{value}' is not a {syntax}.") else: return value @@ -269,8 +266,7 @@ class ConfigNode(object): elif value.lower() == 'false': return False else: - raise ValueError("Syntax error, '%s' is not %s." - % (value, syntax)) + raise ValueError(f"Syntax error, '{value}' is not {syntax}.") def ui_type_loglevel(self, value=None, enum=False, reverse=False): ''' @@ -304,8 +300,7 @@ class ConfigNode(object): elif value in type_enum: return value else: - raise ValueError("Syntax error, '%s' is not %s" - % (value, syntax)) + raise ValueError(f"Syntax error, '{value}' is not {syntax}") def ui_type_color(self, value=None, enum=False, reverse=False): ''' @@ -329,7 +324,7 @@ class ConfigNode(object): else: return 'default' - type_enum = self.shell.con.colors + ['default'] + type_enum = [*self.shell.con.colors, 'default'] syntax = '|'.join(type_enum) if value is None: if enum: @@ -341,8 +336,7 @@ class ConfigNode(object): elif value in type_enum: return value else: - raise ValueError("Syntax error, '%s' is not %s" - % (value, syntax)) + raise ValueError(f"Syntax error, '{value}' is not {syntax}") def ui_type_colordefault(self, value=None, enum=False, reverse=False): ''' @@ -366,7 +360,7 @@ class ConfigNode(object): else: return 'none' - type_enum = self.shell.con.colors + ['none'] + type_enum = [*self.shell.con.colors, 'none'] syntax = '|'.join(type_enum) if value is None: if enum: @@ -378,8 +372,7 @@ class ConfigNode(object): elif value in type_enum: return value else: - raise ValueError("Syntax error, '%s' is not %s" - % (value, syntax)) + raise ValueError(f"Syntax error, '{value}' is not {syntax}") # User interface get/set methods @@ -425,7 +418,7 @@ class ConfigNode(object): ''' return self.shell.prefs[parameter] - def ui_eval_param(self, ui_value, type, default): + def ui_eval_param(self, ui_value, type, default): # noqa: A002 ''' Evaluates a user-provided parameter value using a given type helper. If the parameter value is None, the default will be returned. If the @@ -434,8 +427,8 @@ class ConfigNode(object): @param ui_value: The user provided parameter value. @type ui_value: str - @param type: The ui_type to be used - @type type: str + @param type_: The ui_type to be used + @type type_: str @param default: The default value to return. @type default: any @return: The evaluated parameter value. @@ -453,11 +446,11 @@ class ConfigNode(object): else: return value - def get_type_method(self, type): + def get_type_method(self, type): # noqa: A002 ''' Returns the type helper method matching the type name. ''' - return getattr(self, "%s%s" % (self.ui_type_method_prefix, type)) + return getattr(self, f"{self.ui_type_method_prefix}{type}") # User interface commands @@ -481,49 +474,46 @@ class ConfigNode(object): self.shell.con.epy_write(''' AVAILABLE CONFIGURATION GROUPS ============================== - %s - ''' % ' '.join(self.list_config_groups())) + {}\n + '''.format(' '.join(self.list_config_groups()))) elif not parameter: if group not in self.list_config_groups(): - raise ExecutionError("Unknown configuration group: %s" % group) + raise ExecutionError(f"Unknown configuration group: {group}") - section = "%s CONFIG GROUP" % group.upper() + section = f"{group.upper()} CONFIG GROUP" underline1 = ''.ljust(len(section), '=') parameters = '' for p_name in self.list_group_params(group, writable=True): p_def = self.get_group_param(group, p_name) type_method = self.get_type_method(p_def['type']) - p_name = "%s=%s" % (p_def['name'], p_def['type']) + p_name = f"{p_def['name']}={p_def['type']}" underline2 = ''.ljust(len(p_name), '-') - parameters += '%s\n%s\n%s\n\n' \ - % (p_name, underline2, p_def['description']) - self.shell.con.epy_write('''%s\n%s\n%s\n''' - % (section, underline1, parameters)) + parameters += f"{p_name}\n{underline2}\n{p_def['description']}\n\n" + self.shell.con.epy_write(f"{section}\n{underline1}\n{parameters}") elif group not in self.list_config_groups(): - raise ExecutionError("Unknown configuration group: %s" % group) + raise ExecutionError(f"Unknown configuration group: {group}") - for param, value in six.iteritems(parameter): + for param, value in parameter.items(): if param not in self.list_group_params(group): - raise ExecutionError("Unknown parameter %s in group '%s'." - % (param, group)) + raise ExecutionError(f"Unknown parameter {param} in group '{group}'.") p_def = self.get_group_param(group, param) type_method = self.get_type_method(p_def['type']) if not p_def['writable']: - raise ExecutionError("Parameter %s is read-only." % param) + raise ExecutionError(f"Parameter {param} is read-only.") try: value = type_method(value) except ValueError as msg: - raise ExecutionError("Not setting %s! %s" % (param, msg)) + raise ExecutionError(f"Not setting {param}! {msg}") group_setter = self.get_group_setter(group) group_setter(param, value) group_getter = self.get_group_getter(group) value = group_getter(param) value = type_method(value, reverse=True) - self.shell.con.display("Parameter %s is now '%s'." % (param, value)) + self.shell.con.display(f"Parameter {param} is now '{value}'.") def ui_complete_set(self, parameters, text, current_param): ''' @@ -539,8 +529,9 @@ class ConfigNode(object): ''' completions = [] - self.shell.log.debug("Called with params=%s, text='%s', current='%s'" - % (str(parameters), text, current_param)) + self.shell.log.debug(f"Called with params={parameters!s}, " + f"text='{text}', " + f"current='{current_param}'") if current_param == 'group': completions = [group for group in self.list_config_groups() @@ -567,7 +558,7 @@ class ConfigNode(object): if len(completions) == 1 and not completions[0].endswith('='): completions = [completions[0] + ' '] - self.shell.log.debug("Returning completions %s." % str(completions)) + self.shell.log.debug(f"Returning completions {completions!s}.") return completions def ui_command_get(self, group=None, *parameter): @@ -589,13 +580,13 @@ class ConfigNode(object): self.shell.con.epy_write(''' AVAILABLE CONFIGURATION GROUPS ============================== - %s - ''' % ' '.join(self.list_config_groups())) + {}\n + '''.format(' '.join(self.list_config_groups()))) elif not parameter: if group not in self.list_config_groups(): - raise ExecutionError("Unknown configuration group: %s" % group) + raise ExecutionError(f"Unknown configuration group: {group}") - section = "%s CONFIG GROUP" % group.upper() + section = f"{group.upper()} CONFIG GROUP" underline1 = ''.ljust(len(section), '=') parameters = '' params = [self.get_group_param(group, p_name) @@ -605,23 +596,20 @@ class ConfigNode(object): value = group_getter(p_def['name']) type_method = self.get_type_method(p_def['type']) value = type_method(value, reverse=True) - param = "%s=%s" % (p_def['name'], value) + param = f"{p_def['name']}={value}" if p_def['writable'] is False: param += " [ro]" underline2 = ''.ljust(len(param), '-') - parameters += '%s\n%s\n%s\n\n' \ - % (param, underline2, p_def['description']) + parameters += f"{param}\n{underline2}\n{p_def['description']}\n\n" - self.shell.con.epy_write('''%s\n%s\n%s\n''' - % (section, underline1, parameters)) + self.shell.con.epy_write(f"{section}\n{underline1}\n{parameters}") elif group not in self.list_config_groups(): - raise ExecutionError("Unknown configuration group: %s" % group) + raise ExecutionError(f"Unknown configuration group: {group}") for param in parameter: if param not in self.list_group_params(group): - raise ExecutionError("No parameter '%s' in group '%s'." - % (param, group)) + raise ExecutionError(f"No parameter '{param}' in group '{group}'.") self.shell.log.debug("About to get the parameter's value.") group_getter = self.get_group_getter(group) @@ -629,12 +617,8 @@ class ConfigNode(object): p_def = self.get_group_param(group, param) type_method = self.get_type_method(p_def['type']) value = type_method(value, reverse=True) - if p_def['writable']: - writable = "" - else: - writable = "[ro]" - self.shell.con.display("%s=%s %s" - % (param, value, writable)) + writable = '' if p_def['writable'] else '[ro]' + self.shell.con.display(f"{param}={value} {writable}") def ui_complete_get(self, parameters, text, current_param): ''' @@ -650,8 +634,9 @@ class ConfigNode(object): ''' completions = [] - self.shell.log.debug("Called with params=%s, text='%s', current='%s'" - % (str(parameters), text, current_param)) + self.shell.log.debug(f"Called with params={parameters!s}, " + f"text='{text}', " + f"current='{current_param}'") if current_param == 'group': completions = [group for group in self.list_config_groups() @@ -669,7 +654,7 @@ class ConfigNode(object): if len(completions) == 1 and not completions[0].endswith('='): completions = [completions[0] + ' '] - self.shell.log.debug("Returning completions %s." % str(completions)) + self.shell.log.debug(f"Returning completions {completions!s}.") return completions def ui_command_ls(self, path=None, depth=None): @@ -740,9 +725,7 @@ class ConfigNode(object): else: root_call = False - if do_list: - color = None - elif not level % 3: + if do_list or not level % 3: color = None elif not (level - 1) % 3: color = 'blue' @@ -832,7 +815,7 @@ class ConfigNode(object): line += name if self.shell.prefs['tree_status_mode']: - line += ' %s%s' % (pad, summary) + line += f' {pad}{summary}' lines.append(line) paths.append(root.path) @@ -842,14 +825,14 @@ class ConfigNode(object): and not do_list: tree = '' for child in children: - tree = tree + self._render_tree(child, [False], depth) + tree += self._render_tree(child, [False], depth) else: tree = line + '\n' if depth is None or depth > 0: if depth is not None: - depth = depth - 1 + depth -= 1 for i in range(len(children)): - margin.append(i<len(children)-1) + margin.append(i < len(children) - 1) if do_list: new_lines, new_paths = \ self._render_tree(children[i], margin, depth, @@ -857,8 +840,7 @@ class ConfigNode(object): lines.extend(new_lines) paths.extend(new_paths) else: - tree = tree \ - + self._render_tree(children[i], margin, depth) + tree += self._render_tree(children[i], margin, depth) margin.pop() if root_call: @@ -866,14 +848,13 @@ class ConfigNode(object): return (lines, paths) else: return tree[:-1] + elif do_list: + return (lines, paths) else: - if do_list: - return (lines, paths) - else: - return tree + return tree - def ui_complete_ls(self, parameters, text, current_param): + def ui_complete_ls(self, parameters, text, current_param): # noqa: ARG002 TODO ''' Parameter auto-completion method for user command ls. @param parameters: Parameters on the command line. @@ -887,7 +868,7 @@ class ConfigNode(object): ''' if current_param == 'path': (basedir, slash, partial_name) = text.rpartition('/') - basedir = basedir + slash + basedir += slash target = self.get_node(basedir) names = [child.name for child in target.children] completions = [] @@ -896,22 +877,21 @@ class ConfigNode(object): if name.startswith(partial_name): num_matches += 1 if num_matches == 1: - completions.append("%s%s/" % (basedir, name)) + completions.append(f"{basedir}{name}/") else: - completions.append("%s%s" % (basedir, name)) - if len(completions) == 1: - if not self.get_node(completions[0]).children: - completions[0] = completions[0].rstrip('/') + ' ' + completions.append(f"{basedir}{name}") + if len(completions) == 1 and not self.get_node(completions[0]).children: + completions[0] = completions[0].rstrip('/') + ' ' # Bookmarks bookmarks = ['@' + bookmark for bookmark in self.shell.prefs['bookmarks'] if ('@' + bookmark).startswith(text)] - self.shell.log.debug("Found bookmarks %s." % str(bookmarks)) + self.shell.log.debug(f"Found bookmarks {bookmarks!s}.") if bookmarks: completions.extend(bookmarks) - self.shell.log.debug("Completions are %s." % str(completions)) + self.shell.log.debug(f"Completions are {completions!s}.") return completions elif current_param == 'depth': @@ -921,9 +901,10 @@ class ConfigNode(object): except ValueError: self.shell.log.debug("Text is not a number.") return [] - return [ text + number for number + return [text + number for number in [str(num) for num in range(10)] if (text + number).startswith(text)] + return None def ui_command_cd(self, path=None): ''' @@ -960,7 +941,7 @@ class ConfigNode(object): ======== ls cd ''' - self.shell.log.debug("Changing current node to '%s'." % path) + self.shell.log.debug(f"Changing current node to '{path}'.") if self.shell.prefs['path_history'] is None: self.shell.prefs['path_history'] = [self.path] @@ -974,8 +955,7 @@ class ConfigNode(object): exists = False while not exists: if self.shell.prefs['path_history_index'] > 0: - self.shell.prefs['path_history_index'] = \ - self.shell.prefs['path_history_index'] - 1 + self.shell.prefs['path_history_index'] -= 1 index = self.shell.prefs['path_history_index'] path = self.shell.prefs['path_history'][index] try: @@ -989,7 +969,7 @@ class ConfigNode(object): self.shell.prefs['path_history_index'] = 0 self.shell.prefs['path_history'][0] = '/' exists = True - self.shell.log.info('Taking you back to %s.' % path) + self.shell.log.info(f'Taking you back to {path}.') return self.get_node(path) # Go forward in history @@ -1002,8 +982,7 @@ class ConfigNode(object): while not exists: if self.shell.prefs['path_history_index'] \ < len(self.shell.prefs['path_history']) - 1: - self.shell.prefs['path_history_index'] = \ - self.shell.prefs['path_history_index'] + 1 + self.shell.prefs['path_history_index'] += 1 index = self.shell.prefs['path_history_index'] path = self.shell.prefs['path_history'][index] try: @@ -1018,10 +997,9 @@ class ConfigNode(object): = len(self.shell.prefs['path_history']) self.shell.prefs['path_history'].append(path) exists = True - self.shell.log.info('Taking you back to %s.' % path) + self.shell.log.info(f'Taking you back to {path}.') return self.get_node(path) - # Use an urwid walker to select the path if path is None: lines, paths = self._render_tree(self.get_root(), do_list=True) start_pos = paths.index(self.path) @@ -1038,7 +1016,7 @@ class ConfigNode(object): if target_node.path != self.shell.prefs['path_history'][index]: # Truncate the hostory to retain current path as last one self.shell.prefs['path_history'] = \ - self.shell.prefs['path_history'][:index+1] + self.shell.prefs['path_history'][:index + 1] # Append the new path and update the index self.shell.prefs['path_history'].append(target_node.path) self.shell.prefs['path_history_index'] = index + 1 @@ -1049,7 +1027,7 @@ class ConfigNode(object): def _lines_walker(self, lines, start_pos): ''' - Using the curses urwid library, displays all lines passed as argument, + Using the curses library, displays all lines passed as argument, and after allowing selection of one line using up, down and enter keys, returns its index. @param lines: The lines to display and select from. @@ -1059,36 +1037,43 @@ class ConfigNode(object): @return: the index of the selected line. @rtype: int ''' - import urwid - palette = [('header', 'white', 'black'), - ('reveal focus', 'black', 'yellow', 'standout')] + def draw_menu(stdscr, current_row): + stdscr.clear() + height, width = stdscr.getmaxyx() + + for idx, row in enumerate(lines): + x = width // 2 - len(row) // 2 + y = height // 2 - len(lines) // 2 + idx + if idx == current_row: + stdscr.attron(curses.color_pair(1)) + stdscr.addstr(y, x, row) + stdscr.attroff(curses.color_pair(1)) + else: + stdscr.addstr(y, x, row) - content = urwid.SimpleListWalker( - [urwid.AttrMap(w, None, 'reveal focus') - for w in [urwid.Text(line) for line in lines]]) + stdscr.refresh() - listbox = urwid.ListBox(content) - frame = urwid.Frame(listbox) + def main(stdscr): + curses.start_color() + curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_YELLOW) + current_row = start_pos + draw_menu(stdscr, current_row) - def handle_input(input, raw): - for key in input: - widget, pos = content.get_focus() - if key == 'up': - if pos > 0: - content.set_focus(pos-1) - elif key == 'down': - try: - content.set_focus(pos+1) - except IndexError: - pass - elif key == 'enter': - raise urwid.ExitMainLoop() + while True: + key = stdscr.getch() + if key == curses.KEY_UP and current_row > 0: + current_row -= 1 + elif key == curses.KEY_DOWN and current_row < len(lines) - 1: + current_row += 1 + elif key == curses.KEY_ENTER or key in (10, 13): + break + + draw_menu(stdscr, current_row) - content.set_focus(start_pos) - loop = urwid.MainLoop(frame, palette, input_filter=handle_input) - loop.run() - return listbox.focus_position + return current_row + + return curses.wrapper(main) def ui_complete_cd(self, parameters, text, current_param): ''' @@ -1104,9 +1089,10 @@ class ConfigNode(object): ''' if current_param == 'path': completions = self.ui_complete_ls(parameters, text, current_param) - completions.extend([nav for nav in ['<', '>'] + completions.extend([nav for nav in ('<', '>') if nav.startswith(text)]) return completions + return None def ui_command_help(self, topic=None): ''' @@ -1124,30 +1110,31 @@ class ConfigNode(object): ''') for command in commands: - msg += " - %s\n" % self.get_command_syntax(command)[0] + msg += f" - {self.get_command_syntax(command)[0]}\n" + msg += "\n" self.shell.con.epy_write(msg) return if topic not in commands: - raise ExecutionError("Cannot find help topic %s." % topic) + raise ExecutionError(f"Cannot find help topic {topic}.") syntax, comments, defaults = self.get_command_syntax(topic) - msg = self.shell.con.dedent(''' + msg = self.shell.con.dedent(f''' SYNTAX ====== - %s + {syntax} - ''' % syntax) + ''') for comment in comments: msg += comment + '\n' if defaults: - msg += self.shell.con.dedent(''' + msg += self.shell.con.dedent(f''' DEFAULT VALUES ============== - %s + {defaults} - ''' % defaults) + ''') msg += self.shell.con.dedent(''' DESCRIPTION =========== @@ -1156,7 +1143,7 @@ class ConfigNode(object): msg += "\n" self.shell.con.epy_write(msg) - def ui_complete_help(self, parameters, text, current_param): + def ui_complete_help(self, parameters, text, current_param): # noqa: ARG002 TODO ''' Parameter auto-completion method for user command help. @param parameters: Parameters on the command line. @@ -1226,24 +1213,25 @@ class ConfigNode(object): ''' if action == 'add' and bookmark: if bookmark in self.shell.prefs['bookmarks']: - raise ExecutionError("Bookmark %s already exists." % bookmark) + raise ExecutionError(f"Bookmark {bookmark} already exists.") self.shell.prefs['bookmarks'][bookmark] = self.path # No way Prefs is going to account for that :-( self.shell.prefs.save() - self.shell.log.info("Bookmarked %s as %s." - % (self.path, bookmark)) + self.shell.log.info(f"Bookmarked {self.path} as {bookmark}.") + return None elif action == 'del' and bookmark: if bookmark not in self.shell.prefs['bookmarks']: - raise ExecutionError("No such bookmark %s." % bookmark) + raise ExecutionError(f"No such bookmark {bookmark}.") del self.shell.prefs['bookmarks'][bookmark] # No way Prefs is going to account for that deletion self.shell.prefs.save() - self.shell.log.info("Deleted bookmark %s." % bookmark) + self.shell.log.info(f"Deleted bookmark {bookmark}.") + return None elif action == 'go' and bookmark: if bookmark not in self.shell.prefs['bookmarks']: - raise ExecutionError("No such bookmark %s." % bookmark) + raise ExecutionError(f"No such bookmark {bookmark}.") return self.ui_command_cd( self.shell.prefs['bookmarks'][bookmark]) elif action == 'show': @@ -1256,12 +1244,13 @@ class ConfigNode(object): bookmarks += "No bookmarks yet.\n" else: for (bookmark, path) \ - in six.iteritems(self.shell.prefs['bookmarks']): + in self.shell.prefs['bookmarks'].items(): if len(bookmark) == 1: bookmark += '\0' underline = ''.ljust(len(bookmark), '-') - bookmarks += "%s\n%s\n%s\n\n" % (bookmark, underline, path) + bookmarks += f"{bookmark}\n{underline}\n{path}\n\n" self.shell.con.epy_write(bookmarks) + return None else: raise ExecutionError("Syntax error, see 'help bookmarks'.") @@ -1278,14 +1267,13 @@ class ConfigNode(object): @rtype: list of str ''' if current_param == 'action': - completions = [action for action in ['add', 'del', 'go', 'show'] + completions = [action for action in ('add', 'del', 'go', 'show') if action.startswith(text)] elif current_param == 'bookmark': - if 'action' in parameters: - if parameters['action'] not in ['show', 'add']: - completions = [mark for mark - in self.shell.prefs['bookmarks'] - if mark.startswith(text)] + if 'action' in parameters and parameters['action'] not in {'show', 'add'}: + completions = [mark for mark + in self.shell.prefs['bookmarks'] + if mark.startswith(text)] else: completions = [] @@ -1375,7 +1363,7 @@ class ConfigNode(object): ''' return ('', None) - def execute_command(self, command, pparams=[], kparams={}): + def execute_command(self, command, pparams=[], kparams={}): # noqa: B006 TODO Do not use mutable data structures for argument defaults ''' Execute a user command on the node. This works by finding out which is the support command method, using ConfigNode naming convention: @@ -1393,14 +1381,14 @@ class ConfigNode(object): they are interpreted by ConfigShell. @rtype: str or ConfigNode or None ''' - self.shell.log.debug("Executing command %s " % command - + "with pparams %s " % pparams - + "and kparams %s." % kparams) + self.shell.log.debug(f"Executing command {command} " + f"with pparams {pparams} " + f"and kparams {kparams}.") if command in self.list_commands(): method = self.get_command_method(command) else: - raise ExecutionError("Command not found %s" % command) + raise ExecutionError(f"Command not found {command}") self.assert_params(method, pparams, kparams) return method(*pparams, **kparams) @@ -1417,10 +1405,10 @@ class ConfigNode(object): @type kparams: dict @raise ExecutionError: When the check fails. ''' - spec = inspect.getargspec(method) + spec = inspect.getfullargspec(method) args = spec.args[1:] pp = spec.varargs - kw = spec.keywords + kw = spec.varkw if spec.defaults is None: nb_opt_params = 0 @@ -1444,38 +1432,35 @@ class ConfigNode(object): self.shell.log.debug("Min params: %d" % nb_min_params) self.shell.log.debug("Max params: %d" % nb_max_params) - self.shell.log.debug("Required params: %s" % ", ".join(req_params)) - self.shell.log.debug("Optional params: %s" % ", ".join(opt_params)) - self.shell.log.debug("Got %s standard params." % nb_standard_params) - self.shell.log.debug("Got %s extended params." % nb_extended_params) - self.shell.log.debug("Variable positional params: %s" % pp) - self.shell.log.debug("Variable keyword params: %s" % kw) + self.shell.log.debug(f"Required params: {', '.join(req_params)}") + self.shell.log.debug(f"Optional params: {', '.join(opt_params)}") + self.shell.log.debug(f"Got {nb_standard_params} standard params.") + self.shell.log.debug(f"Got {nb_extended_params} extended params.") + self.shell.log.debug(f"Variable positional params: {pp}") + self.shell.log.debug(f"Variable keyword params: {kw}") if len(missing_params) == 1: raise ExecutionError( - "Missing required parameter %s" - % missing_params[0]) - elif missing_params: + f"Missing required parameter {missing_params[0]}") + if missing_params: raise ExecutionError( - "Missing required parameters %s" - % ", ".join("'%s'" % missing for missing in missing_params)) + "Missing required parameters {}".format(", ".join( + f"'{missing}'" for missing in missing_params))) - if spec.keywords is None: + if kw is None: if len(unexpected_keywords) == 1: raise ExecutionError( - "Unexpected keyword parameter '%s'." - % unexpected_keywords[0]) - elif unexpected_keywords: + f"Unexpected keyword parameter '{unexpected_keywords[0]}'.") + if unexpected_keywords: raise ExecutionError( - "Unexpected keyword parameters %s." - % ", ".join("'%s'" % kw for kw in unexpected_keywords)) + "Unexpected keyword parameters {}.".format(", ".join( + f"'{kw}'" for kw in unexpected_keywords))) all_params = args[:len(pparams)] all_params.extend(kparams.keys()) for param in all_params: if all_params.count(param) > 1: raise ExecutionError( - "Duplicate parameter %s." - % param) + f"Duplicate parameter {param}.") if nb_opt_params == 0 \ and nb_standard_params != nb_min_params \ @@ -1508,7 +1493,7 @@ class ConfigNode(object): @rtype: method object ''' prefix = self.ui_getgroup_method_prefix - return getattr(self, '%s%s' % (prefix, group)) + return getattr(self, f'{prefix}{group}') def get_group_setter(self, group): ''' @@ -1518,7 +1503,7 @@ class ConfigNode(object): @rtype: method object ''' prefix = self.ui_setgroup_method_prefix - return getattr(self, '%s%s' % (prefix, group)) + return getattr(self, f'{prefix}{group}') def get_command_method(self, command): ''' @@ -1530,11 +1515,10 @@ class ConfigNode(object): ''' prefix = self.ui_command_method_prefix if command in self.list_commands(): - return getattr(self, '%s%s' % (prefix, command)) + return getattr(self, f'{prefix}{command}') else: - self.shell.log.debug('No command named %s in %s (%s)' - % (command, self.name, self.path)) - raise ValueError('No command named "%s".' % command) + self.shell.log.debug(f'No command named {command} in {self.name} ({self.path})') + raise ValueError(f'No command named "{command}".') def get_completion_method(self, command): ''' @@ -1545,7 +1529,7 @@ class ConfigNode(object): ''' prefix = self.ui_complete_method_prefix try: - method = getattr(self, '%s%s' % (prefix, command)) + method = getattr(self, f'{prefix}{command}') except AttributeError: return None else: @@ -1575,12 +1559,12 @@ class ConfigNode(object): @type command: str ''' method = self.get_command_method(command) - parameters, args, kwargs, default = inspect.getargspec(method) - parameters = parameters[1:] - if default is None: + spec = inspect.getfullargspec(method) + parameters = spec.args[1:] + if spec.defaults is None: num_defaults = 0 else: - num_defaults = len(default) + num_defaults = len(spec.defaults) if num_defaults != 0: required_parameters = parameters[:-num_defaults] @@ -1589,32 +1573,32 @@ class ConfigNode(object): required_parameters = parameters optional_parameters = [] - self.shell.log.debug("Required: %s" % str(required_parameters)) - self.shell.log.debug("Optional: %s" % str(optional_parameters)) + self.shell.log.debug(f"Required: {required_parameters!s}") + self.shell.log.debug(f"Optional: {optional_parameters!s}") - syntax = "%s " % command + syntax = f"{command} " required_parameters_str = '' for param in required_parameters: - required_parameters_str += "%s " % param + required_parameters_str += f"{param} " syntax += required_parameters_str optional_parameters_str = '' for param in optional_parameters: - optional_parameters_str += "[%s] " % param + optional_parameters_str += f"[{param}] " syntax += optional_parameters_str comments = [] - if args is not None: - syntax += "[%s...] " % args - if kwargs is not None: - syntax += "[%s=value...] " % (kwargs) + if spec.varargs is not None: + syntax += f"[{spec.varargs}...] " + if spec.varkw is not None: + syntax += f"[{spec.varkw}=value...] " default_values = '' if num_defaults > 0: for index, param in enumerate(optional_parameters): - if default[index] is not None: - default_values += "%s=%s " % (param, str(default[index])) + if spec.defaults[index] is not None: + default_values += f"{param}={spec.defaults[index]!s} " return syntax, comments, default_values @@ -1630,20 +1614,17 @@ class ConfigNode(object): @rtype: ([str...], bool, bool) ''' method = self.get_command_method(command) - parameters, args, kwargs, default = inspect.getargspec(method) - parameters = parameters[1:] - if args is not None: - free_pparams = args + spec = inspect.getfullargspec(method) + parameters = spec.args[1:] + if spec.varargs is not None: + free_pparams = spec.varargs else: free_pparams = False - if kwargs is not None: - free_kparams = kwargs + if spec.varkw is not None: + free_kparams = spec.varkw else: free_kparams = False - self.shell.log.debug("Signature is %s, %s, %s." - % (str(parameters), - str(free_pparams), - str(free_kparams))) + self.shell.log.debug(f"Signature is {parameters!s}, {free_pparams!s}, {free_kparams!s}.") return parameters, free_pparams, free_kparams def get_root(self): @@ -1656,7 +1637,7 @@ class ConfigNode(object): else: return self.parent.get_root() - def define_config_group_param(self, group, param, type, + def define_config_group_param(self, group, param, type, # noqa: A002 description=None, writable=True): ''' Helper to define configuration group parameters. @@ -1673,7 +1654,7 @@ class ConfigNode(object): self._configuration_groups[group] = {} if description is None: - description = "The %s %s parameter." % (param, group) + description = f"The {param} {group} parameter." # Fail early if the type and set/get helpers don't exist self.get_type_method(type) @@ -1703,7 +1684,7 @@ class ConfigNode(object): return [] else: params = [] - for p_name, p_def in six.iteritems(self._configuration_groups[group]): + for p_name, p_def in self._configuration_groups[group].items(): (p_type, p_description, p_writable) = p_def if writable is not None and p_writable != writable: continue @@ -1724,15 +1705,14 @@ class ConfigNode(object): @raise ValueError: If the parameter or group does not exist. ''' if group not in self.list_config_groups(): - raise ValueError("Not such configuration group %s" % group) + raise ValueError(f"Not such configuration group {group}") if param not in self.list_group_params(group): - raise ValueError("Not such parameter %s in configuration group %s" - % (param, group)) + raise ValueError(f"Not such parameter {param} in configuration group {group}") (p_type, p_description, p_writable) = \ self._configuration_groups[group][param] - return dict(name=param, group=group, type=p_type, - description=p_description, writable=p_writable) + return {'name': param, 'group': group, 'type': p_type, + 'description': p_description, 'writable': p_writable} shell = property(_get_shell, doc="Gets the shell attached to ConfigNode tree.") @@ -1754,10 +1734,7 @@ class ConfigNode(object): @return: Wether or not we are a root node. @rtype: bool ''' - if self._parent is None: - return True - else: - return False + return self._parent is None def get_child(self, name): ''' @@ -1770,9 +1747,7 @@ class ConfigNode(object): for child in self._children: if child.name == name: return child - else: - raise ValueError("No such path %s/%s" - % (self.path.rstrip('/'), name)) + raise ValueError(f"No such path {self.path.rstrip('/')}/{name}") def remove_child(self, child): ''' @@ -1816,13 +1791,13 @@ class ConfigNode(object): if bookmark in self.shell.prefs['bookmarks']: path = self.shell.prefs['bookmarks'][bookmark] else: - raise ValueError("No such bookmark %s" % bookmark) + raise ValueError(f"No such bookmark {bookmark}") # More cleanup - path = re.sub('%s+' % self._path_separator, self._path_separator, path) + path = re.sub(f'{self._path_separator}+', self._path_separator, path) if len(path) > 1: path = path.rstrip(self._path_separator) - self.shell.log.debug("Looking for path '%s'" % path) + self.shell.log.debug(f"Looking for path '{path}'") # Absolute path - make relative and pass on to root node diff --git a/configshell/prefs.py b/configshell/prefs.py index 7faa1fe..384e6ba 100644 --- a/configshell/prefs.py +++ b/configshell/prefs.py @@ -15,10 +15,12 @@ License for the specific language governing permissions and limitations under the License. ''' -import six import fcntl +import pickle +from pathlib import Path -class Prefs(object): + +class Prefs: ''' This is a preferences backend object used to: - Hold the ConfigShell preferences @@ -73,10 +75,7 @@ class Prefs(object): @param key: The preferences dictionnary key to check. @type key: any valid dict key ''' - if key in self._prefs: - return True - else: - return False + return key in self._prefs def __delitem__(self, key): ''' @@ -115,7 +114,7 @@ class Prefs(object): @return: Iterates on the items in preferences. @rtype: yields items that are (key, value) pairs ''' - return six.iteritems(self._prefs) + return self._prefs.items() def save(self, filename=None): ''' @@ -128,12 +127,10 @@ class Prefs(object): filename = self.filename if filename is not None: - fsock = open(filename, 'wb') - fcntl.lockf(fsock, fcntl.LOCK_UN) - try: - six.moves.cPickle.dump(self._prefs, fsock, 2) - finally: - fsock.close() + path = Path(filename) + with path.open('wb') as fsock: + fcntl.lockf(fsock, fcntl.LOCK_UN) + pickle.dump(self._prefs, fsock, 2) def load(self, filename=None): ''' @@ -143,10 +140,8 @@ class Prefs(object): if filename is None: filename = self.filename - if filename is not None: - fsock = open(filename, 'rb') - fcntl.lockf(fsock, fcntl.LOCK_SH) - try: - self._prefs = six.moves.cPickle.load(fsock) - finally: - fsock.close() + if filename is not None and Path(filename).exists(): + path = Path(filename) + with path.open('rb') as fsock: + fcntl.lockf(fsock, fcntl.LOCK_SH) + self._prefs = pickle.load(fsock) # noqa S301 TODO diff --git a/configshell/shell.py b/configshell/shell.py index 437186d..bc9b6e1 100644 --- a/configshell/shell.py +++ b/configshell/shell.py @@ -16,20 +16,28 @@ under the License. ''' import os -import six +import signal import sys -from pyparsing import (alphanums, Empty, Group, locatedExpr, - OneOrMore, Optional, ParseResults, Regex, - Suppress, Word) - -from . import console -from . import log -from . import prefs +from contextlib import suppress +from pathlib import Path + +from pyparsing import ( + OneOrMore, + Optional, + ParseResults, + Regex, + Suppress, + Word, + alphanums, + locatedExpr, +) + +from . import console, log, prefs from .node import ConfigNode, ExecutionError + # A fix for frozen packages -import signal -def handle_sigint(signum, frame): +def handle_sigint(signum, frame): # noqa: ARG001 TODO ''' Raise KeyboardInterrupt when we get a SIGINT. This is normally done by python, but even after patching @@ -38,30 +46,29 @@ def handle_sigint(signum, frame): ''' raise KeyboardInterrupt -try: + +# In a thread, this fails +with suppress(Exception): signal.signal(signal.SIGINT, handle_sigint) -except Exception: - # In a thread, this fails - pass if sys.stdout.isatty(): import readline - tty=True + tty = True else: - tty=False + tty = False # remember the original setting - oldTerm = os.environ.get('TERM') - os.environ['TERM'] = '' - + old_term = os.getenv('TERM') + os.unsetenv('TERM') import readline # restore the orignal TERM setting - if oldTerm != None: - os.environ['TERM'] = oldTerm - del oldTerm + if old_term is not None: + os.putenv('TERM', old_term) + del old_term -class ConfigShell(object): + +class ConfigShell: ''' This is a simple CLI command interpreter that can be used both in interactive or non-interactive modes. @@ -93,7 +100,7 @@ class ConfigShell(object): 'tree_max_depth': 0, 'tree_status_mode': True, 'tree_round_nodes': True, - 'tree_show_root': True + 'tree_show_root': True, } _completion_help_topic = '' @@ -113,50 +120,50 @@ class ConfigShell(object): # Grammar of the command line command = locatedExpr(Word(alphanums + '_'))('command') - var = Word(alphanums + '?;&*$!#,=_\+/.<>()~@:-%[]') + var = Word(alphanums + r'?;&*$!#,=_\+/.<>()~@:-%[]') value = var - keyword = Word(alphanums + '_\-') + keyword = Word(alphanums + r'_\-') kparam = locatedExpr(keyword + Suppress('=') + Optional(value, default=''))('kparams*') pparam = locatedExpr(var)('pparams*') parameter = kparam | pparam parameters = OneOrMore(parameter) bookmark = Regex('@([A-Za-z0-9:_.]|-)+') - pathstd = Regex('([A-Za-z0-9:_.\[\]]|-)*' + '/' + '([A-Za-z0-9:_.\[\]/]|-)*') \ + pathstd = Regex(r'([A-Za-z0-9:_.\[\]]|-)*' + '/' + r'([A-Za-z0-9:_.\[\]/]|-)*') \ | '..' | '.' path = locatedExpr(bookmark | pathstd | '*')('path') parser = Optional(path) + Optional(command) + Optional(parameters) self._parser = parser if tty: - readline.set_completer_delims('\t\n ~!#$^&(){}\|;\'",?') + readline.set_completer_delims('\t\n ~!#$^&(){}\\|;\'",?') readline.set_completion_display_matches_hook( self._display_completions) self.log = log.Log() if preferences_dir is not None: - preferences_dir = os.path.expanduser(preferences_dir) - if not os.path.exists(preferences_dir): - os.makedirs(preferences_dir) - self._prefs_file = preferences_dir + '/prefs.bin' - self.prefs = prefs.Prefs(self._prefs_file) - self._cmd_history = preferences_dir + '/history.txt' + preferences_dir_path = Path(preferences_dir) + if not preferences_dir_path.exists(): + preferences_dir_path.mkdir(parents=True) + self._prefs_file = preferences_dir_path / 'prefs.bin' + self.prefs = prefs.Prefs(str(self._prefs_file)) + self._cmd_history = preferences_dir_path / '/history.txt' self._save_history = True - if not os.path.isfile(self._cmd_history): + cmd_history_path = self._cmd_history + if not cmd_history_path.is_file(): try: - open(self._cmd_history, 'w').close() - except: - self.log.warning("Cannot create history file %s, " - % self._cmd_history - + "command history will not be saved.") + with cmd_history_path.open('w'): + pass + except OSError: + self.log.warning(f"Cannot create history file {self._cmd_history}, " + f"command history will not be saved.") self._save_history = False - if os.path.isfile(self._cmd_history) and tty: + if self._cmd_history.is_file() and tty: try: readline.read_history_file(self._cmd_history) - except IOError: - self.log.warning("Cannot read command history file %s." - % self._cmd_history) + except OSError: + self.log.warning(f"Cannot read command history file {self._cmd_history}.") if self.prefs['logfile'] is None: self.prefs['logfile'] = preferences_dir + '/' + 'log.txt' @@ -169,11 +176,10 @@ class ConfigShell(object): try: self.prefs.load() - except IOError: - self.log.warning("Could not load preferences file %s." - % self._prefs_file) + except OSError: + self.log.warning(f"Could not load preferences file {self._prefs_file}.") - for pref, value in six.iteritems(self.default_prefs): + for pref, value in self.default_prefs.items(): if pref not in self.prefs: self.prefs[pref] = value @@ -181,7 +187,7 @@ class ConfigShell(object): # Private methods - def _display_completions(self, substitution, matches, max_length): + def _display_completions(self, substitution, matches, max_length): # noqa: ARG002 TODO ''' Display the completions. Invoked by readline. @param substitution: string to complete @@ -239,19 +245,19 @@ class ConfigShell(object): else: nr_cols = 1 - for i in six.moves.range(0, len(matches), nr_cols): + for i in range(0, len(matches), nr_cols): self.con.raw_write(''.join(matches[i:i+nr_cols])) self.con.raw_write('\n') # Display the prompt and the command line - line = "%s%s" % (self._get_prompt(), readline.get_line_buffer()) - self.con.raw_write("%s" % line) + line = f"{self._get_prompt()}{readline.get_line_buffer()}" + self.con.raw_write(line) # Move the cursor where it should be y_pos = self.con.get_cursor_xy()[1] self.con.set_cursor_xy(x_orig, y_pos) - def _complete_token_command(self, text, path, command): + def _complete_token_command(self, text, path, command): # TODO # noqa: ARG002 ''' Completes a partial command token, which could also be the beginning of a path. @@ -267,12 +273,11 @@ class ConfigShell(object): completions = [] target = self._current_node.get_node(path) commands = target.list_commands() - self.log.debug("Completing command token among %s" % str(commands)) + self.log.debug(f"Completing command token among {commands!s}") # Start with the possible commands - for command in commands: - if command.startswith(text): - completions.append(command) + completions = [command for command in commands if command.startswith(text)] + if len(completions) == 1: completions[0] = completions[0] + ' ' @@ -303,7 +308,7 @@ class ConfigShell(object): self.con.render_text( 'command', self.prefs['color_command']) if len(path_completions) == 1 and \ - not path_completions[0][-1] in [' ', '*'] and \ + path_completions[0][-1] not in (' ', '*') and \ not self._current_node.get_node(path_completions[0]).children: path_completions[0] = path_completions[0] + ' ' completions.extend(path_completions) @@ -314,12 +319,11 @@ class ConfigShell(object): # Even a bookmark bookmarks = ['@' + bookmark for bookmark in self.prefs['bookmarks'] - if bookmark.startswith("%s" % text.lstrip('@'))] - self.log.debug("Found bookmarks %s." % str(bookmarks)) + if bookmark.startswith(text.lstrip('@'))] + self.log.debug(f"Found bookmarks {bookmarks!s}.") if bookmarks: completions.extend(bookmarks) - # We are done return completions @@ -335,33 +339,32 @@ class ConfigShell(object): if text.endswith('.'): text = text + '/' (basedir, slash, partial_name) = text.rpartition('/') - self.log.debug("Got basedir=%s, partial_name=%s" - % (basedir, partial_name)) + self.log.debug(f"Got basedir={basedir}, partial_name={partial_name}") basedir = basedir + slash target = self._current_node.get_node(basedir) names = [child.name for child in target.children] # Iterall path completion - if names and partial_name in ['', '*']: + if names and partial_name in ('', '*'): # Not suggesting iterall to end a path that has only one # child allows for fast TAB action to add the only child's # name. if len(names) > 1: - completions.append("%s* " % basedir) + completions.append(f"{basedir}* ") for name in names: num_matches = 0 if name.startswith(partial_name): num_matches += 1 if num_matches == 1: - completions.append("%s%s/" % (basedir, name)) + completions.append(f"{basedir}{name}/") else: - completions.append("%s%s" % (basedir, name)) + completions.append(f"{basedir}{name}") # Bookmarks bookmarks = ['@' + bookmark for bookmark in self.prefs['bookmarks'] - if bookmark.startswith("%s" % text.lstrip('@'))] - self.log.debug("Found bookmarks %s." % str(bookmarks)) + if bookmark.startswith(text.lstrip('@'))] + self.log.debug(f"Found bookmarks {bookmarks!s}.") if bookmarks: completions.extend(bookmarks) @@ -402,12 +405,11 @@ class ConfigShell(object): for index in range(len(pparams)): if index < len(cmd_params): current_parameters[cmd_params[index]] = pparams[index] - for key, value in six.iteritems(kparams): + for key, value in kparams.items(): current_parameters[key] = value self._completion_help_topic = command completion_method = target.get_completion_method(command) - self.log.debug("Command %s accepts parameters %s." - % (command, cmd_params)) + self.log.debug(f"Command {command} accepts parameters {cmd_params}.") # Do we still accept positional params ? pparam_ok = True @@ -436,7 +438,7 @@ class ConfigShell(object): else: pparam_index = len(pparams) - 1 self._current_parameter = cmd_params[pparam_index] - self.log.debug("Completing pparam %s." % self._current_parameter) + self.log.debug(f"Completing pparam {self._current_parameter}.") if completion_method: pparam_completions = completion_method( current_parameters, text, self._current_parameter) @@ -453,10 +455,8 @@ class ConfigShell(object): if param not in kparams \ if param.startswith(text)] - self.log.debug("Possible pparam values are %s." - % str(completions)) - self.log.debug("Possible kparam keywords are %s." - % str(keyword_completions)) + self.log.debug(f"Possible pparam values are {completions!s}.") + self.log.debug(f"Possible kparam keywords are {keyword_completions!s}.") if keyword_completions: if self._current_parameter: @@ -471,14 +471,13 @@ class ConfigShell(object): self._current_token = \ self.con.render_text( 'keyword=', self.prefs['color_keyword']) + elif self._current_parameter: + self._current_token = \ + self.con.render_text( + self._current_parameter, + self.prefs['color_parameter']) else: - if self._current_parameter: - self._current_token = \ - self.con.render_text( - self._current_parameter, - self.prefs['color_parameter']) - else: - self._current_token = '' + self._current_token = '' completions.extend(keyword_completions) @@ -506,7 +505,7 @@ class ConfigShell(object): self._current_parameter = 'free_parameter' if do_free_kparams: - if not 'keyword=' in self._current_token: + if 'keyword=' not in self._current_token: self._current_token = \ self.con.render_text( 'keyword=', self.prefs['color_keyword']) \ @@ -517,7 +516,7 @@ class ConfigShell(object): completions.extend(free_completions) - self.log.debug("Found completions %s." % str(completions)) + self.log.debug(f"Found completions {completions!s}.") return completions def _complete_token_kparam(self, text, path, command, pparams, kparams): @@ -536,21 +535,19 @@ class ConfigShell(object): @return: Possible completions for the token. @rtype: list of str ''' - self.log.debug("Called for text='%s'" % text) + self.log.debug(f"Called for text='{text}'") target = self._current_node.get_node(path) cmd_params = target.get_command_signature(command)[0] - self.log.debug("Command %s accepts parameters %s." - % (command, cmd_params)) + self.log.debug(f"Command {command} accepts parameters {cmd_params}.") (keyword, sep, current_value) = text.partition('=') - self.log.debug("Completing '%s' for kparam %s" - % (current_value, keyword)) + self.log.debug(f"Completing '{current_value}' for kparam {keyword}") self._current_parameter = keyword current_parameters = {} for index in range(len(pparams)): current_parameters[cmd_params[index]] = pparams[index] - for key, value in six.iteritems(kparams): + for key, value in kparams.items(): current_parameters[key] = value completion_method = target.get_completion_method(command) if completion_method: @@ -563,9 +560,9 @@ class ConfigShell(object): self.con.render_text( self._current_parameter, self.prefs['color_parameter']) - self.log.debug("Found completions %s." % str(completions)) + self.log.debug(f"Found completions {completions!s}.") - return ["%s=%s" % (keyword, completion) for completion in completions] + return [f"{keyword}={completion}" for completion in completions] def _complete(self, text, state): ''' @@ -617,8 +614,7 @@ class ConfigShell(object): pparams, kparams, text, current_token) - self.log.debug("Returning completions %s to readline." - % str(self._current_completions)) + self.log.debug(f"Returning completions {self._current_completions!s} to readline.") if state < len(self._current_completions): return self._current_completions[state] @@ -653,12 +649,9 @@ class ConfigShell(object): ''' completions = [] - self.log.debug("Dispatching completion for %s token. " - % current_token - + "text='%s', path='%s', command='%s', " - % (text, path, command) - + "pparams=%s, kparams=%s" - % (str(pparams), str(kparams))) + self.log.debug(f"Dispatching completion for {current_token} token. " + f"text='{text}', path='{path}', command='{command}', " + f"pparams={pparams!s}, kparams={kparams!s}") (path, iterall) = path.partition('*')[:2] if iterall: @@ -687,8 +680,7 @@ class ConfigShell(object): self._complete_token_kparam(text, cpl_path, command, pparams, kparams) else: - self.log.debug("Cannot complete unknown token %s." - % current_token) + self.log.debug(f"Cannot complete unknown token {current_token}.") return completions @@ -701,14 +693,12 @@ class ConfigShell(object): if prompt_length and prompt_length < len(prompt_path): half = (prompt_length - 3) // 2 - prompt_path = "%s...%s" \ - % (prompt_path[:half], prompt_path[-half:]) + prompt_path = f"{prompt_path[:half]}...{prompt_path[-half:]}" if 'prompt_msg' in dir(self._current_node): - return "%s%s> " % (self._current_node.prompt_msg(), - prompt_path) + return f"{self._current_node.prompt_msg()}{prompt_path}> " else: - return "%s> " % prompt_path + return f"{prompt_path}> " def _cli_loop(self): ''' @@ -722,7 +712,7 @@ class ConfigShell(object): try: readline.parse_and_bind("tab: complete") readline.set_completer(self._complete) - cmdline = six.moves.input(self._get_prompt()).strip() + cmdline = input(self._get_prompt()).strip() except EOFError: self.con.raw_write('exit\n') cmdline = "exit" @@ -730,10 +720,9 @@ class ConfigShell(object): if self._save_history: try: readline.write_history_file(self._cmd_history) - except IOError: + except OSError: self.log.warning( - "Cannot write to command history file %s." \ - % self._cmd_history) + f"Cannot write to command history file {self._cmd_history}.") self.log.warning( "Saving command history has been disabled!") self._save_history = False @@ -765,9 +754,8 @@ class ConfigShell(object): if isinstance(parse_results.kparams, ParseResults): kparams = dict([kparam.value for kparam in parse_results.kparams]) - self.log.debug("Parse gave path='%s' command='%s' " % (path, command) - + "pparams=%s " % str(pparams) - + "kparams=%s" % str(kparams)) + self.log.debug(f"Parse gave path='{path}' command='{command}' " + f"pparams={pparams!s} kparams={kparams!s}") return (parse_results, path, command, pparams, kparams) def _execute_command(self, path, command, pparams, kparams): @@ -814,15 +802,15 @@ class ConfigShell(object): targets = target.children for target in targets: if iterall: - self.con.display("[%s]" % target.path) + self.con.display(f"[{target.path}]") result = target.execute_command(command, pparams, kparams) - self.log.debug("Command execution returned %r" % result) + self.log.debug(f"Command execution returned {result!r}") if isinstance(result, ConfigNode): self._current_node = result elif result == 'EXIT': self._exit = True elif result is not None: - raise ExecutionError("Unexpected result: %r" % result) + raise ExecutionError(f"Unexpected result: {result!r}") # Public methods @@ -838,7 +826,7 @@ class ConfigShell(object): @type cmdline: str ''' if cmdline: - self.log.verbose("Running command line '%s'." % cmdline) + self.log.verbose(f"Running command line '{cmdline}'.") path, command, pparams, kparams = self._parse_cmdline(cmdline)[1:] self._execute_command(path, command, pparams, kparams) @@ -852,12 +840,10 @@ class ConfigShell(object): @type exit_on_error: bool ''' try: - script_fd = open(script_path, 'r') - self.run_stdin(script_fd, exit_on_error) - except IOError as msg: - raise IOError(msg) - finally: - script_fd.close() + with Path(script_path).open() as script_fd: + self.run_stdin(script_fd, exit_on_error) + except OSError as e: + raise OSError(e) def run_stdin(self, file_descriptor=sys.stdin, exit_on_error=True): ''' @@ -872,7 +858,7 @@ class ConfigShell(object): for cmdline in file_descriptor: try: self.run_cmdline(cmdline.strip()) - except Exception as msg: + except Exception as msg: # noqa: PERF203 `try`-`except` within a loop incurs performance overhead self.log.error(msg) if exit_on_error is True: raise ExecutionError("Aborting run on error.") diff --git a/configshell_fb b/configshell_fb deleted file mode 120000 index 76ca383..0000000 --- a/configshell_fb +++ /dev/null @@ -1 +0,0 @@ -configshell
\ No newline at end of file diff --git a/configshell_fb.py b/configshell_fb.py new file mode 100644 index 0000000..a6fd918 --- /dev/null +++ b/configshell_fb.py @@ -0,0 +1,12 @@ +# Providing backwards compatibility for modules importing 'configshell_fb' + +from configshell import ConfigNode, ConfigShell, Console, ExecutionError, Log, Prefs + +__all__ = [ + 'Console', + 'Log', + 'ConfigNode', + 'ExecutionError', + 'Prefs', + 'ConfigShell', +] diff --git a/debian/changelog b/debian/changelog index 94d3354..5a0436f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,12 @@ +python-configshell-fb (1:2.0.0-1) unstable; urgency=medium + + * Team upload + * Use PyPI as source for upstream tarballs + * New upstream version 2.0.0 (Closes: #1057841, #1087405) + * Drop now unnecessary patches + + -- Chris Hofstaedtler <[email protected]> Sun, 29 Dec 2024 02:16:44 +0100 + python-configshell-fb (1:1.1.28-2.1) unstable; urgency=medium [ Bas Couwenberg ] diff --git a/debian/control b/debian/control index 65b09fb..c8cd3eb 100644 --- a/debian/control +++ b/debian/control @@ -2,15 +2,18 @@ Source: python-configshell-fb Section: python Priority: optional Maintainer: Linux Block Storage Team <[email protected]> -Uploaders: Christophe Vu-Brugier <[email protected]>, - Ritesh Raj Sarraf <[email protected]>, - Christian Seiler <[email protected]> -Build-Depends: debhelper-compat (= 13), - dh-python, - python3-all, - python3-pyparsing, - python3-setuptools, - python3-six +Uploaders: + Christophe Vu-Brugier <[email protected]>, + Ritesh Raj Sarraf <[email protected]>, + Christian Seiler <[email protected]>, +Build-Depends: + debhelper-compat (= 13), + dh-python, + pybuild-plugin-pyproject, + python3-all, + python3-hatch-vcs, + python3-hatchling, + python3-pyparsing, Standards-Version: 4.5.1 Homepage: https://github.com/open-iscsi/configshell-fb Vcs-Git: https://salsa.debian.org/linux-blocks-team/python-configshell-fb.git @@ -18,11 +21,10 @@ Vcs-Browser: https://salsa.debian.org/linux-blocks-team/python-configshell-fb Package: python3-configshell-fb Architecture: all -Depends: ${misc:Depends}, - ${python3:Depends}, - python3-pyparsing, - python3-six, - python3-urwid, +Depends: + python3-pyparsing, + ${misc:Depends}, + ${python3:Depends}, Description: Python library for building configuration shells - Python 3 The configshell-fb package is a Python library that provides a framework for building simple but nice CLI-based applications. diff --git a/debian/patches/replace-getargspec-with-getfullargspec.patch b/debian/patches/replace-getargspec-with-getfullargspec.patch deleted file mode 100644 index 8388066..0000000 --- a/debian/patches/replace-getargspec-with-getfullargspec.patch +++ /dev/null @@ -1,31 +0,0 @@ -Description: replace getargspec() with getfullargspec() - inspect.getargspec() doesn't work anymore with Python3.11 -Author: Maurizio Lombardi <[email protected]> -Origin: https://github.com/open-iscsi/configshell-fb/commit/f3ac914861bd605e3d634aeeb5e706abdbd39259 -Forwarded: not-needed -Bug-Debian: https://bugs.debian.org/1028996 - ---- a/configshell/node.py -+++ b/configshell/node.py -@@ -1417,10 +1417,10 @@ class ConfigNode(object): - @type kparams: dict - @raise ExecutionError: When the check fails. - ''' -- spec = inspect.getargspec(method) -+ spec = inspect.getfullargspec(method) - args = spec.args[1:] - pp = spec.varargs -- kw = spec.keywords -+ kw = spec.varkw - - if spec.defaults is None: - nb_opt_params = 0 -@@ -1460,7 +1460,7 @@ class ConfigNode(object): - "Missing required parameters %s" - % ", ".join("'%s'" % missing for missing in missing_params)) - -- if spec.keywords is None: -+ if kw is None: - if len(unexpected_keywords) == 1: - raise ExecutionError( - "Unexpected keyword parameter '%s'." diff --git a/debian/patches/replace-more-occurrences-of-getargspec-with-getfullargspec.patch b/debian/patches/replace-more-occurrences-of-getargspec-with-getfullargspec.patch deleted file mode 100644 index b50db4e..0000000 --- a/debian/patches/replace-more-occurrences-of-getargspec-with-getfullargspec.patch +++ /dev/null @@ -1,70 +0,0 @@ -Description: Replace more occurrences of getargspec() with getfullargspec() - Follow up for f3ac914861bd605e3d634aeeb5e706abdbd39259, getargspec was used at two more places. -Author: Vojtech Trefny <[email protected]> -Origin: https://github.com/open-iscsi/configshell-fb/commit/343e46cbbbed339c67fe969cdf443af4c979f43e -Forwarded: not-needed -Bug-Debian: https://bugs.debian.org/1028996 - ---- a/configshell/node.py -+++ b/configshell/node.py -@@ -1575,12 +1575,12 @@ class ConfigNode(object): - @type command: str - ''' - method = self.get_command_method(command) -- parameters, args, kwargs, default = inspect.getargspec(method) -- parameters = parameters[1:] -- if default is None: -+ spec = inspect.getfullargspec(method) -+ parameters = spec.args[1:] -+ if spec.defaults is None: - num_defaults = 0 - else: -- num_defaults = len(default) -+ num_defaults = len(spec.defaults) - - if num_defaults != 0: - required_parameters = parameters[:-num_defaults] -@@ -1605,16 +1605,16 @@ class ConfigNode(object): - syntax += optional_parameters_str - - comments = [] -- if args is not None: -- syntax += "[%s...] " % args -- if kwargs is not None: -- syntax += "[%s=value...] " % (kwargs) -+ if spec.varargs is not None: -+ syntax += "[%s...] " % spec.varargs -+ if spec.varkw is not None: -+ syntax += "[%s=value...] " % (spec.varkw) - - default_values = '' - if num_defaults > 0: - for index, param in enumerate(optional_parameters): -- if default[index] is not None: -- default_values += "%s=%s " % (param, str(default[index])) -+ if spec.defaults[index] is not None: -+ default_values += "%s=%s " % (param, str(spec.defaults[index])) - - return syntax, comments, default_values - -@@ -1630,14 +1630,14 @@ class ConfigNode(object): - @rtype: ([str...], bool, bool) - ''' - method = self.get_command_method(command) -- parameters, args, kwargs, default = inspect.getargspec(method) -- parameters = parameters[1:] -- if args is not None: -- free_pparams = args -+ spec = inspect.getfullargspec(method) -+ parameters = spec.args[1:] -+ if spec.varargs is not None: -+ free_pparams = spec.varargs - else: - free_pparams = False -- if kwargs is not None: -- free_kparams = kwargs -+ if spec.varkw is not None: -+ free_kparams = spec.varkw - else: - free_kparams = False - self.shell.log.debug("Signature is %s, %s, %s." diff --git a/debian/patches/series b/debian/patches/series deleted file mode 100644 index 0d2ee57..0000000 --- a/debian/patches/series +++ /dev/null @@ -1,2 +0,0 @@ -replace-getargspec-with-getfullargspec.patch -replace-more-occurrences-of-getargspec-with-getfullargspec.patch diff --git a/debian/watch b/debian/watch index bd73046..a42145d 100644 --- a/debian/watch +++ b/debian/watch @@ -1,3 +1,2 @@ version=4 -opts=uversionmangle=s/fb//,dversionmangle=s/fb//,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/configshell-fb-$1\.tar\.gz/ \ - https://github.com/open-iscsi/configshell-fb/releases .*/v?(\d\S+)\.tar\.gz +https://pypi.debian.net/configshell-fb/configshell_fb-(.+)\.(?:tar\.(?:gz|bz2|xz)) diff --git a/examples/myshell b/examples/myshell index c932e28..93d7542 100755 --- a/examples/myshell +++ b/examples/myshell @@ -17,7 +17,9 @@ under the License. ''' import os -import configshell_fb as configshell + +import configshell + class MySystemRoot(configshell.node.ConfigNode): def __init__(self, shell): @@ -166,7 +168,7 @@ class Users(configshell.node.ConfigNode): def main(): shell = configshell.shell.ConfigShell('~/.myshell') - root_node = MySystemRoot(shell) + MySystemRoot(shell) shell.run_interactive() if __name__ == "__main__": diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ec37959 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,100 @@ +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling", "hatch-vcs"] + +[project] +name = "configshell-fb" +description = "A framework to implement simple but nice CLIs." +readme = "README.md" +license = "Apache-2.0" +requires-python = ">=3.9" +authors = [ + {email = "[email protected]", name = "Andy Grover"}, + {email = "[email protected]", name = "Jerome Martin"} +] +maintainers = [{email = "[email protected]", name = "Maurizio Lombardi"}] +classifiers = [ + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "pyparsing>=2.4.7, <4", # 2.4.7 required for EL9 compatibility +] +dynamic = ["version"] + +[project.license-files] +paths = ["COPYING"] + +[project.urls] +Homepage = "http://github.com/open-iscsi/configshell-fb" + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.targets.wheel] +packages = [ + "configshell", + "configshell_fb.py" +] + +[tool.hatch.envs.default] +dependencies = [ + "ruff", + "pre-commit", +] + +[tool.ruff] +line-length = 100 + +[tool.ruff.lint] +select = [ + "F", # Pyflakes + "E", # pycodestyle error + "W", # pycodestyle warning + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + "YTT", # flake8-2020 + "S", # flake8-bandit + "B", # flake8-bugbear + "A", # flake8-builtins + "COM", # flake8-commas + "C4", # flake8-comprehensions + "EXE", # flake8-executable + "FA", # flake8-future-annotations + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "LOG", # flake8-logging + "PIE", # flake8-pie + "Q003", # flake8-quotes avoidable-escaped-quote + "Q004", # flake8-quotes unnecessary-escaped-quote + "RSE", # flake8-raise + "RET", # flake8-return + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "INT", # flake8-gettext + "ARG", # flake8-unused-argument + "PTH", # flake8-use-pathlib + "PL", # Pylint + "FLY", # flynt + "PERF", # Perflint + "FURB", # refurb + "RUF", # Ruff +] +ignore = [ + "RUF012", # TODO Mutable class attributes should be annotated with `typing.ClassVar` + "B904", # raise-without-from-inside-except + "PLR09", # Too many branches/statements/arguments + "S105", # Possible hardcoded password assigned + "RET505", # Unnecessary `else` after `return` statement + "PLW2901", # TODO `for` loop variable overwritten by assignment target + "UP031", # TODO Use format specifiers instead of percent format + "SIM102", # Use a single if statement instead of nested if statements + "SIM108", # Use ternary operator {contents} instead of if-else-block +] + +[tool.ruff.lint.per-file-ignores] +# Possible hardcoded password assigned to: "current_token" +"configshell/shell" = ["S105"] diff --git a/setup.py b/setup.py deleted file mode 100755 index 9b5c7d1..0000000 --- a/setup.py +++ /dev/null @@ -1,54 +0,0 @@ -#! /usr/bin/env python -''' -This file is part of ConfigShell. -Copyright (c) 2011-2013 by Datera, Inc - -Licensed under the Apache License, Version 2.0 (the "License"); you may -not use this file except in compliance with the License. You may obtain -a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -License for the specific language governing permissions and limitations -under the License. -''' - -import os -import re -from setuptools import setup - -# Get version without importing. -init_file_path = os.path.join(os.path.dirname(__file__), 'configshell/__init__.py') - -with open(init_file_path) as f: - for line in f: - match = re.match(r"__version__.*'([0-9.]+)'", line) - if match: - version = match.group(1) - break - else: - raise Exception("Couldn't find version in setup.py") - -setup( - name = 'configshell-fb', - version = '1.1.28', - description = 'A framework to implement simple but nice CLIs.', - license = 'Apache 2.0', - maintainer = 'Andy Grover', - maintainer_email = '[email protected]', - url = 'http://github.com/open-iscsi/configshell-fb', - packages = ['configshell', 'configshell_fb'], - install_requires = [ - 'pyparsing >= 2.0.2', - 'six', - 'urwid', - ], - classifiers = [ - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "License :: OSI Approved :: Apache Software License", - ], - ) |
