diff --git a/.pylintrc b/.pylintrc index ab2dad0..b1acd1a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -10,7 +10,8 @@ # R0401 cyclic-import # R0205 useless-object-inheritance # R1717 consider-using-dict-comprehension -disable=W0511,C0111,C0103,C0415,I0011,R0913,R0903,R0401,R0205,R1717,useless-suppression +disable=W0511,C0111,C0103,C0415,I0011,R0913,R0903,R0401,R0205,R1717,useless-suppression, + consider-using-f-string [FORMAT] max-line-length=120 @@ -23,4 +24,3 @@ max-branches=20 [SIMILARITIES] min-similarity-lines=10 - diff --git a/.travis.yml b/.travis.yml index 7dd059e..6f160b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,5 +8,6 @@ python: - '3.7' - '3.8' - '3.9' +- '3.10' install: pip install tox-travis script: tox diff --git a/azure-pipeline.yml b/azure-pipeline.yml index 4966b3e..4fe216c 100644 --- a/azure-pipeline.yml +++ b/azure-pipeline.yml @@ -17,7 +17,7 @@ jobs: timeoutInMinutes: 20 pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-20.04' strategy: matrix: Python36: @@ -32,6 +32,9 @@ jobs: Python39: python.version: '3.9' tox_env: 'py39' + Python310: + python.version: '3.10' + tox_env: 'py310' steps: - task: UsePythonVersion@0 displayName: 'Use Python $(python.version)' @@ -50,12 +53,12 @@ jobs: - job: BuildPythonWheel condition: succeeded() pool: - image: 'ubuntu-16.04' + vmImage: 'ubuntu-20.04' steps: - task: UsePythonVersion@0 - displayName: Use Python 3.6 + displayName: Use Python 3.9 inputs: - versionSpec: 3.6 + versionSpec: 3.9 - bash: | set -ev diff --git a/knack/commands.py b/knack/commands.py index b1f7eef..2843f69 100644 --- a/knack/commands.py +++ b/knack/commands.py @@ -180,9 +180,9 @@ def __init__(self, cli_ctx=None, command_cls=CLICommand, excluded_command_handle self.skip_applicability = False self.excluded_command_handler_args = excluded_command_handler_args # A command table is a dictionary of name -> CLICommand instances - self.command_table = dict() + self.command_table = {} # A command group table is a dictionary of names -> CommandGroup instances - self.command_group_table = dict() + self.command_group_table = {} # An argument registry stores all arguments for commands self.argument_registry = ArgumentRegistry() self.extra_argument_registry = defaultdict(lambda: {}) diff --git a/knack/config.py b/knack/config.py index 2c5e580..258cdc1 100644 --- a/knack/config.py +++ b/knack/config.py @@ -10,6 +10,8 @@ _UNSET = object() +CONFIG_FILE_ENCODING = 'utf-8' + def get_config_parser(): return configparser.ConfigParser() # keep this for backward compatibility @@ -190,7 +192,7 @@ def __init__(self, config_dir, config_path, config_comment=None): self.config_comment = config_comment self.config_parser = configparser.ConfigParser() if os.path.exists(config_path): - self.config_parser.read(config_path) + self.config_parser.read(config_path, encoding=CONFIG_FILE_ENCODING) def items(self, section): return self.config_parser.items(section) if self.config_parser else [] @@ -220,7 +222,7 @@ def getboolean(self, section, option): def set(self, config): ensure_dir(self.config_dir) - with open(self.config_path, 'w') as configfile: + with open(self.config_path, 'w', encoding=CONFIG_FILE_ENCODING) as configfile: if self.config_comment: configfile.write(self.config_comment + '\n') config.write(configfile) diff --git a/knack/help.py b/knack/help.py index ee09652..9d2740d 100644 --- a/knack/help.py +++ b/knack/help.py @@ -362,7 +362,7 @@ def __init__(self, name_source, description, required, choices=None, default=Non def update_from_data(self, data): if self.name != data.get('name'): - raise HelpAuthoringException(u"mismatched name {} vs. {}" + raise HelpAuthoringException("mismatched name {} vs. {}" .format(self.name, data.get('name'))) @@ -394,7 +394,7 @@ def _print_header(self, cli_name, help_file): _print_indent('Command' if help_file.type == 'command' else 'Group', indent) indent += 1 - LINE_FORMAT = u'{cli}{name}{separator}{summary}' + LINE_FORMAT = '{cli}{name}{separator}{summary}' line = LINE_FORMAT.format( cli=cli_name, name=' ' + help_file.command if help_file.command else '', @@ -421,7 +421,7 @@ def _build_long_summary(item): def _print_groups(self, help_file): - LINE_FORMAT = u'{name}{padding}{tags}{separator}{summary}' + LINE_FORMAT = '{name}{padding}{tags}{separator}{summary}' indent = 1 self.max_line_len = 0 @@ -496,13 +496,13 @@ def _print_items(layouts): @staticmethod def _get_choices_defaults_sources_str(p): - choice_str = u' Allowed values: {}.'.format(', '.join(sorted([str(x) for x in p.choices]))) \ + choice_str = ' Allowed values: {}.'.format(', '.join(sorted([str(x) for x in p.choices]))) \ if p.choices else '' - default_str = u' Default: {}.'.format(p.default) \ + default_str = ' Default: {}.'.format(p.default) \ if p.default and p.default != argparse.SUPPRESS else '' - value_sources_str = u' Values from: {}.'.format(', '.join(p.value_sources)) \ + value_sources_str = ' Values from: {}.'.format(', '.join(p.value_sources)) \ if p.value_sources else '' - return u'{}{}{}'.format(choice_str, default_str, value_sources_str) + return '{}{}{}'.format(choice_str, default_str, value_sources_str) @staticmethod def print_description_list(help_files): @@ -510,11 +510,11 @@ def print_description_list(help_files): max_length = max(len(f.name) for f in help_files) if help_files else 0 for help_file in sorted(help_files, key=lambda h: h.name): column_indent = max_length - len(help_file.name) - _print_indent(u'{}{}{}'.format(help_file.name, - ' ' * column_indent, - FIRST_LINE_PREFIX + help_file.short_summary - if help_file.short_summary - else ''), + _print_indent('{}{}{}'.format(help_file.name, + ' ' * column_indent, + FIRST_LINE_PREFIX + help_file.short_summary + if help_file.short_summary + else ''), indent, _get_hanging_indent(max_length, indent)) @@ -524,14 +524,14 @@ def _print_examples(help_file): _print_indent('Examples', indent) for e in help_file.examples: indent = 1 - _print_indent(u'{0}'.format(e.name), indent) + _print_indent('{0}'.format(e.name), indent) indent = 2 - _print_indent(u'{0}'.format(e.text), indent) + _print_indent('{0}'.format(e.text), indent) print('') def _print_arguments(self, help_file): # pylint: disable=too-many-statements - LINE_FORMAT = u'{name}{padding}{tags}{separator}{short_summary}' + LINE_FORMAT = '{name}{padding}{tags}{separator}{short_summary}' indent = 1 self.max_line_len = 0 @@ -644,9 +644,9 @@ def _build_long_summary(item): group_registry = ArgumentGroupRegistry([p.group_name for p in help_file.parameters if p.group_name]) def _get_parameter_key(parameter): - return u'{}{}{}'.format(group_registry.get_group_priority(parameter.group_name), - str(not parameter.required), - parameter.name) + return '{}{}{}'.format(group_registry.get_group_priority(parameter.group_name), + str(not parameter.required), + parameter.name) parameter_layouts = _layout_items(help_file.parameters) _print_items(parameter_layouts) diff --git a/knack/parser.py b/knack/parser.py index 1e7782f..4883b00 100644 --- a/knack/parser.py +++ b/knack/parser.py @@ -80,7 +80,8 @@ def _expand_prefixed_files(args): if args[arg].startswith('@'): try: logger.debug('Attempting to read file %s', args[arg][1:]) - with open(args[arg][1:], 'r') as f: + # Use the default system encoding: https://docs.python.org/3/library/functions.html#open + with open(args[arg][1:], 'r') as f: # pylint: disable=unspecified-encoding content = f.read() args[arg] = content except IOError: diff --git a/requirements.txt b/requirements.txt index a607494..39172ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ argcomplete==1.12.2 colorama==0.4.4 -flake8==3.8.4 +flake8==4.0.1 jmespath==0.10.0 Pygments==2.8.1 -pylint==2.7.2 -pytest==6.2.2 +pylint==2.11.1 +pytest==6.2.5 PyYAML tabulate==0.8.9 vcrpy==4.1.1 diff --git a/scripts/license_verify.py b/scripts/license_verify.py index 72d3ba9..ec56ae5 100644 --- a/scripts/license_verify.py +++ b/scripts/license_verify.py @@ -37,7 +37,7 @@ def get_files_without_header(): for a_file in files: if a_file.endswith('.py'): cur_file_path = os.path.join(current_dir, a_file) - with open(cur_file_path, 'r') as f: + with open(cur_file_path, 'r', encoding='utf-8') as f: file_text = f.read() if len(file_text) > 0 and not contains_header(file_text): diff --git a/setup.py b/setup.py index 606956b..db40d67 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'License :: OSI Approved :: MIT License', ], packages=['knack', 'knack.testsdk'], diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index 327ad0e..f9b7be5 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -63,7 +63,7 @@ def test_deprecate_command_group_help(self): with self.assertRaises(SystemExit): self.cli_ctx.invoke('-h'.split()) actual = self.io.getvalue() - expected = u""" + expected = """ Group {} @@ -429,7 +429,7 @@ def test_deprecate_options_execute_expired_non_deprecated(self): """ Ensure non-expired options can be used without warning. """ self.cli_ctx.invoke('arg-test --arg1 foo --opt1 bar --opt5 foo'.split()) actual = self.io.getvalue() - self.assertTrue(u'--alt5' not in actual and u'--opt5' not in actual) + self.assertTrue('--alt5' not in actual and '--opt5' not in actual) @redirect_io def test_deprecate_options_execute_expiring(self): diff --git a/tests/test_experimental.py b/tests/test_experimental.py index 0a7e878..b3100ca 100644 --- a/tests/test_experimental.py +++ b/tests/test_experimental.py @@ -69,7 +69,7 @@ def test_experimental_command_group_help(self): with self.assertRaises(SystemExit): self.cli_ctx.invoke('-h'.split()) actual = self.io.getvalue() - expected = u""" + expected = """ Group {} diff --git a/tests/test_preview.py b/tests/test_preview.py index 7fef236..fa9f90c 100644 --- a/tests/test_preview.py +++ b/tests/test_preview.py @@ -61,7 +61,7 @@ def test_preview_command_group_help(self): with self.assertRaises(SystemExit): self.cli_ctx.invoke('-h'.split()) actual = self.io.getvalue() - expected = u""" + expected = """ Group {} diff --git a/tox.ini b/tox.ini index fd8f7eb..d2f0040 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36,py37,py38,py39 +envlist = py36,py37,py38,py39,py310 [testenv] deps = -rrequirements.txt commands=