From 1ad75ebcec22d6e58834e37204edb5f423868281 Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 31 Aug 2020 00:05:06 +0300 Subject: [PATCH 1/4] Drop support for Python 3.5 --- .github/workflows/test.yml | 2 +- README.rst | 2 +- docs/index.rst | 2 +- setup.py | 7 +++---- tox.ini | 10 +++++----- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 91d87f6c..3e62215c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] - python-version: [3.5, 3.6, 3.7, 3.8, 3.9-dev] + python-version: [3.6, 3.7, 3.8, 3.9-dev] steps: - uses: actions/checkout@v2 diff --git a/README.rst b/README.rst index 088b5f01..179345d4 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ docstring conventions. `PEP 257 `_ out of the box, but it should not be considered a reference implementation. -**pydocstyle** supports Python 3.5, 3.6, 3.7 and 3.8. +**pydocstyle** supports Python 3.6, 3.7 and 3.8. Quick Start diff --git a/docs/index.rst b/docs/index.rst index dfe636ed..991a01e4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,7 +8,7 @@ docstring conventions. `PEP 257 `_ out of the box, but it should not be considered a reference implementation. -**pydocstyle** supports Python 3.5, 3.6, 3.7 and 3.8. +**pydocstyle** supports Python 3.6, 3.7 and 3.8. .. include:: quickstart.rst diff --git a/setup.py b/setup.py index 80369721..501afe3e 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ from setuptools import setup -import sys # Do not update the version manually - it is managed by `bumpversion`. version = '5.1.2rc' @@ -24,14 +23,14 @@ 'Environment :: Console', 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3 :: Only', 'Operating System :: OS Independent', 'License :: OSI Approved :: MIT License', ], - python_requires='>=3.5', + python_requires='>=3.6', keywords='pydocstyle, PEP 257, pep257, PEP 8, pep8, docstrings', packages=('pydocstyle',), package_dir={'': 'src'}, @@ -43,6 +42,6 @@ ], }, project_urls={ - 'Release Notes': 'http://www.pydocstyle.org/en/latest/release_notes.html', + 'Release Notes': 'https://www.pydocstyle.org/en/latest/release_notes.html', }, ) diff --git a/tox.ini b/tox.ini index 7d85e7ed..e6d08656 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # install tox" and then run "tox" from this directory. [tox] -envlist = {py35,py36,py37,py38,py39}-{tests,install},docs,install,py36-docs +envlist = {py36,py37,py38,py39}-{tests,install},docs,install,py36-docs [testenv] download = true @@ -42,10 +42,6 @@ commands = {[testenv:docs]commands} # There's no way to generate sub-sections in tox. # The following sections are all references to the `py37-install`. -[testenv:py35-install] -skip_install = {[testenv:install]skip_install} -commands = {[testenv:install]commands} - [testenv:py36-install] skip_install = {[testenv:install]skip_install} commands = {[testenv:install]commands} @@ -58,6 +54,10 @@ commands = {[testenv:install]commands} skip_install = {[testenv:install]skip_install} commands = {[testenv:install]commands} +[testenv:py39-install] +skip_install = {[testenv:install]skip_install} +commands = {[testenv:install]commands} + [pytest] pep8ignore = test.py E701 E704 From 361f9a330f849e731329bb8e7df92b0f36283483 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 31 Aug 2020 00:07:07 +0300 Subject: [PATCH 2/4] Upgrade Python syntax with pyupgrade --py36-plus --- docs/conf.py | 2 -- src/pydocstyle/checker.py | 2 +- src/pydocstyle/config.py | 8 ++++---- src/pydocstyle/parser.py | 13 ++++++------- src/pydocstyle/violations.py | 8 ++++---- src/tests/test_cases/canonical_google_examples.py | 2 +- src/tests/test_cases/noqa.py | 1 - src/tests/test_cases/test.py | 1 - src/tests/test_cases/unicode_literals.py | 2 -- src/tests/test_definitions.py | 2 +- src/tests/test_integration.py | 7 ++----- 11 files changed, 19 insertions(+), 29 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 05583dac..6f6412a5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # pydocstyle documentation build configuration file, created by # sphinx-quickstart on Fri Jan 30 20:30:42 2015. # diff --git a/src/pydocstyle/checker.py b/src/pydocstyle/checker.py index 3cabc9a9..5f7847d5 100644 --- a/src/pydocstyle/checker.py +++ b/src/pydocstyle/checker.py @@ -985,7 +985,7 @@ def check(filenames, select=None, ignore=None, ignore_decorators=None, ignore_in code = getattr(error, 'code', None) if code in checked_codes: yield error - except (EnvironmentError, AllError, ParseError) as error: + except (OSError, AllError, ParseError) as error: log.warning('Error in file %s: %s', filename, error) yield error except tk.TokenError: diff --git a/src/pydocstyle/config.py b/src/pydocstyle/config.py index 60d11f00..6e789d92 100644 --- a/src/pydocstyle/config.py +++ b/src/pydocstyle/config.py @@ -316,7 +316,7 @@ def _read_configuration_file(self, path): continue if opt.replace('_', '-') not in self.CONFIG_FILE_OPTIONS: - log.warning("Unknown option '{}' ignored".format(opt)) + log.warning(f"Unknown option '{opt}' ignored") continue normalized_opt = opt.replace('-', '_') @@ -335,7 +335,7 @@ def _read_configuration_file(self, path): if options is not None: if not self._validate_options(options): - raise IllegalConfiguration('in file: {}'.format(path)) + raise IllegalConfiguration(f'in file: {path}') return options, should_inherit @@ -388,7 +388,7 @@ def _create_check_config(cls, options, use_defaults=True): kwargs = dict(checked_codes=checked_codes) for key in ('match', 'match_dir', 'ignore_decorators'): - kwargs[key] = getattr(cls, 'DEFAULT_{}_RE'.format(key.upper())) \ + kwargs[key] = getattr(cls, f'DEFAULT_{key.upper()}_RE') \ if getattr(options, key) is None and use_defaults \ else getattr(options, key) return CheckConfiguration(**kwargs) @@ -521,7 +521,7 @@ def _get_set(value_str): """ return cls._expand_error_codes( - set([x.strip() for x in value_str.split(",")]) - {""} + {x.strip() for x in value_str.split(",")} - {""} ) for opt in optional_set_options: diff --git a/src/pydocstyle/parser.py b/src/pydocstyle/parser.py index 1b17c36c..a6afa863 100644 --- a/src/pydocstyle/parser.py +++ b/src/pydocstyle/parser.py @@ -55,7 +55,7 @@ def __eq__(self, other): def __repr__(self): kwargs = ', '.join('{}={!r}'.format(field, getattr(self, field)) for field in self._fields) - return '{}({})'.format(self.__class__.__name__, kwargs) + return f'{self.__class__.__name__}({kwargs})' class Definition(Value): @@ -97,9 +97,9 @@ def is_empty_or_comment(line): return ''.join(reversed(list(filtered_src))) def __str__(self): - out = 'in {} {} `{}`'.format(self._publicity, self._human, self.name) + out = f'in {self._publicity} {self._human} `{self.name}`' if self.skipped_error_codes: - out += ' (skipping {})'.format(self.skipped_error_codes) + out += f' (skipping {self.skipped_error_codes})' return out @@ -211,7 +211,7 @@ def is_public(self): # Check if we are a setter/deleter method, and mark as private if so. for decorator in self.decorators: # Given 'foo', match 'foo.bar' but not 'foobar' or 'sfoo' - if re(r"^{}\.".format(self.name)).match(decorator.name): + if re(fr"^{self.name}\.").match(decorator.name): return False name_is_public = (not self.name.startswith('_') or self.name in VARIADIC_MAGIC_METHODS or @@ -343,7 +343,7 @@ def __init__(self, *args): self.kind = TokenKind(self.kind) def __str__(self): - return "{!r} ({})".format(self.kind, self.value) + return f"{self.kind!r} ({self.value})" class Parser: @@ -472,8 +472,7 @@ def parse_definitions(self, class_, dunder_all=False): yield self.parse_definition(class_._nest(self.current.value)) elif self.current.kind == tk.INDENT: self.consume(tk.INDENT) - for definition in self.parse_definitions(class_): - yield definition + yield from self.parse_definitions(class_) elif self.current.kind == tk.DEDENT: self.consume(tk.DEDENT) return diff --git a/src/pydocstyle/violations.py b/src/pydocstyle/violations.py index 6515d3d6..44ff1949 100644 --- a/src/pydocstyle/violations.py +++ b/src/pydocstyle/violations.py @@ -52,10 +52,10 @@ def set_context(self, definition: Definition, explanation: str) -> None: @property def message(self) -> str: """Return the message to print to the user.""" - ret = '{}: {}'.format(self.code, self.short_desc) + ret = f'{self.code}: {self.short_desc}' if self.context is not None: specific_error_msg = self.context.format(*self.parameters) - ret += ' ({})'.format(specific_error_msg) + ret += f' ({specific_error_msg})' return ret @property @@ -69,7 +69,7 @@ def lines(self) -> str: lines_stripped = list(reversed(list(dropwhile(is_blank, reversed(lines))))) numbers_width = len(str(offset + len(lines_stripped))) - line_format = '{{:{}}}:{{}}'.format(numbers_width) + line_format = f'{{:{numbers_width}}}:{{}}' for n, line in enumerate(lines_stripped): if line: line = ' ' + line @@ -159,7 +159,7 @@ def to_rst(cls) -> str: for group in cls.groups: table += sep_line table += blank_line - table += '|' + '**{}**'.format(group.name).center(max_len + 9) + '|\n' + table += '|' + f'**{group.name}**'.center(max_len + 9) + '|\n' table += blank_line for error in group.errors: table += sep_line diff --git a/src/tests/test_cases/canonical_google_examples.py b/src/tests/test_cases/canonical_google_examples.py index c219191f..301f83c4 100644 --- a/src/tests/test_cases/canonical_google_examples.py +++ b/src/tests/test_cases/canonical_google_examples.py @@ -83,7 +83,7 @@ def fetch_bigtable_rows(big_table, keys, other_silly_variable=None): "('Attributes', not 'Attributes:')") @expect("D407: Missing dashed underline after section ('Attributes')") @expect("D413: Missing blank line after last section ('Attributes')") -class SampleClass(object): +class SampleClass: """Summary of class here. Longer class information.... diff --git a/src/tests/test_cases/noqa.py b/src/tests/test_cases/noqa.py index 7eeeac87..bbbc6a41 100644 --- a/src/tests/test_cases/noqa.py +++ b/src/tests/test_cases/noqa.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # noqa: D400,D415 """Test case for "# noqa" comments""" from .expected import Expectation diff --git a/src/tests/test_cases/test.py b/src/tests/test_cases/test.py index 20b81fce..3df3aba3 100644 --- a/src/tests/test_cases/test.py +++ b/src/tests/test_cases/test.py @@ -1,4 +1,3 @@ -# encoding: utf-8 # No docstring, so we can test D100 from functools import wraps import os diff --git a/src/tests/test_cases/unicode_literals.py b/src/tests/test_cases/unicode_literals.py index 5f7dd700..f0aa466e 100644 --- a/src/tests/test_cases/unicode_literals.py +++ b/src/tests/test_cases/unicode_literals.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- """A module.""" -from __future__ import unicode_literals from .expected import Expectation diff --git a/src/tests/test_definitions.py b/src/tests/test_definitions.py index 83109b5a..b2bd0081 100644 --- a/src/tests/test_definitions.py +++ b/src/tests/test_definitions.py @@ -26,7 +26,7 @@ ]) def test_complex_file(test_case): """Run domain-specific tests from test.py file.""" - case_module = __import__('test_cases.{}'.format(test_case), + case_module = __import__(f'test_cases.{test_case}', globals=globals(), locals=locals(), fromlist=['expectation'], diff --git a/src/tests/test_integration.py b/src/tests/test_integration.py index c5375c79..7de3b755 100644 --- a/src/tests/test_integration.py +++ b/src/tests/test_integration.py @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- - """Use tox or py.test to run the test-suite.""" -from __future__ import with_statement from collections import namedtuple import os @@ -52,7 +49,7 @@ def write_config(self, prefix='', name='tox.ini', **kwargs): self.makedirs(base) with open(os.path.join(base, name), 'wt') as conf: - conf.write("[{}]\n".format(self.script_name)) + conf.write(f"[{self.script_name}]\n") for k, v in kwargs.items(): conf.write("{} = {}\n".format(k.replace('_', '-'), v)) @@ -284,7 +281,7 @@ def test_sectionless_config_file(env): conf.write('[pdcstl]') config_path = conf.name - _, err, code = env.invoke('--config={}'.format(config_path)) + _, err, code = env.invoke(f'--config={config_path}') assert code == 0 assert 'Configuration file does not contain a pydocstyle section' in err From 2cca069730d0ae4e26ef18b36eb00a62926e6ca7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 31 Aug 2020 08:48:34 +0300 Subject: [PATCH 3/4] Drop support for Python 3.5 --- docs/release_notes.rst | 4 ++++ src/tests/parser_test.py | 8 -------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/release_notes.rst b/docs/release_notes.rst index 45cf63af..52c55e16 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -8,6 +8,10 @@ Release Notes Current Development Version --------------------------- +Major Updates + +* Support for Python 3.5 has been dropped (#510). + New Features * Add flag to disable `# noqa` comment processing in API (#485). diff --git a/src/tests/parser_test.py b/src/tests/parser_test.py index 05a329ec..061a3919 100644 --- a/src/tests/parser_test.py +++ b/src/tests/parser_test.py @@ -52,10 +52,6 @@ def do_something(pos_param0, pos_param1, kw_param0="default"): def test_simple_fstring(): """Test parsing of a function with a simple fstring as a docstring.""" - # fstrings are not supported in Python 3.5 - if sys.version_info[0:2] == (3, 5): - return - parser = Parser() code = CodeSnippet("""\ def do_something(pos_param0, pos_param1, kw_param0="default"): @@ -85,10 +81,6 @@ def do_something(pos_param0, pos_param1, kw_param0="default"): def test_fstring_with_args(): """Test parsing of a function with an fstring with args as a docstring.""" - # fstrings are not supported in Python 3.5 - if sys.version_info[0:2] == (3, 5): - return - parser = Parser() code = CodeSnippet("""\ foo = "bar" From d62c3c04ee9aff69b652c3468af788354f0378f5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 31 Aug 2020 12:50:52 +0300 Subject: [PATCH 4/4] Remove D302, redundant now Python 2 is dropped --- src/pydocstyle/checker.py | 25 +------------------------ src/pydocstyle/violations.py | 2 +- src/tests/test_cases/test.py | 10 ---------- 3 files changed, 2 insertions(+), 35 deletions(-) diff --git a/src/pydocstyle/checker.py b/src/pydocstyle/checker.py index 5f7847d5..294a8ce2 100644 --- a/src/pydocstyle/checker.py +++ b/src/pydocstyle/checker.py @@ -385,23 +385,6 @@ def check_backslashes(self, definition, docstring): and not docstring.startswith(('r', 'ur'))): return violations.D301() - @check_for(Definition) - def check_unicode_docstring(self, definition, docstring): - r'''D302: Use u""" for docstrings with Unicode. - - For Unicode docstrings, use u"""Unicode triple-quoted strings""". - - ''' - if 'unicode_literals' in definition.module.future_imports: - return - - # Just check that docstring is unicode, check_triple_double_quotes - # ensures the correct quotes. - if docstring and sys.version_info[0] <= 2: - if not is_ascii(docstring) and not docstring.startswith( - ('u', 'ur')): - return violations.D302() - @staticmethod def _check_ends_with(docstring, chars, violation): """First line ends with one of `chars`. @@ -453,13 +436,7 @@ def check_imperative_mood(self, function, docstring): # def context if check_word in IMPERATIVE_BLACKLIST: return violations.D401b(first_word) - try: - correct_forms = IMPERATIVE_VERBS.get(stem(check_word)) - except UnicodeDecodeError: - # This is raised when the docstring contains unicode - # characters in the first word, but is not a unicode - # string. In which case D302 will be reported. Ignoring. - return + correct_forms = IMPERATIVE_VERBS.get(stem(check_word)) if correct_forms and check_word not in correct_forms: best = max( diff --git a/src/pydocstyle/violations.py b/src/pydocstyle/violations.py index 44ff1949..f9f02f1e 100644 --- a/src/pydocstyle/violations.py +++ b/src/pydocstyle/violations.py @@ -214,7 +214,7 @@ def to_rst(cls) -> str: D300 = D3xx.create_error('D300', 'Use """triple double quotes"""', 'found {0}-quotes') D301 = D3xx.create_error('D301', 'Use r""" if any backslashes in a docstring') -D302 = D3xx.create_error('D302', 'Use u""" for Unicode docstrings') +D302 = D3xx.create_error('D302', 'Deprecated: Use u""" for Unicode docstrings') D4xx = ErrorRegistry.create_group('D4', 'Docstring Content Issues') D400 = D4xx.create_error('D400', 'First line should end with a period', diff --git a/src/tests/test_cases/test.py b/src/tests/test_cases/test.py index 3df3aba3..3d671e59 100644 --- a/src/tests/test_cases/test.py +++ b/src/tests/test_cases/test.py @@ -279,16 +279,6 @@ def exceptions_of_D301(): """ -if sys.version_info[0] <= 2: - @expect('D302: Use u""" for Unicode docstrings') - def unicode_unmarked(): - """Юникод.""" - - @expect('D302: Use u""" for Unicode docstrings') - def first_word_has_unicode_byte(): - """あy.""" - - @expect("D400: First line should end with a period (not 'y')") @expect("D415: First line should end with a period, question mark, " "or exclamation point (not 'y')")