diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 0216f893..06ba095a 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -10,7 +10,11 @@ on: jobs: deploy: runs-on: ubuntu-latest - + environment: + name: pypi + url: https://pypi.org/p/autopep8 + permissions: + id-token: write steps: - uses: actions/checkout@v4 - name: Set up Python @@ -19,12 +23,10 @@ jobs: python-version: '3.x' - name: Install dependencies run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip build pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + - name: Build run: | - python setup.py sdist bdist_wheel - twine upload dist/* + python -m build + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/autopep8.py b/autopep8.py index 8c516e43..fd59890c 100755 --- a/autopep8.py +++ b/autopep8.py @@ -89,7 +89,7 @@ class documentation for more information. import pycodestyle -__version__ = '2.1.1' +__version__ = '2.2.0' CR = '\r' @@ -154,6 +154,14 @@ class documentation for more information. MAX_PYTHON_FILE_DETECTION_BYTES = 1024 +IS_SUPPORT_TOKEN_FSTRING = False +if sys.version_info >= (3, 12): # pgrama: no cover + IS_SUPPORT_TOKEN_FSTRING = True + + +def _custom_formatwarning(message, category, _, __, line=None): + return f"{category.__name__}: {message}\n" + def open_with_encoding(filename, mode='r', encoding=None, limit_byte_check=-1): """Return opened file with a specific encoding.""" @@ -473,6 +481,7 @@ def __init__(self, filename, self.source = sio.readlines() self.options = options self.indent_word = _get_indentword(''.join(self.source)) + self.original_source = copy.copy(self.source) # collect imports line self.imports = {} @@ -524,6 +533,13 @@ def __init__(self, filename, self.fix_w292 = self.fix_w291 self.fix_w293 = self.fix_w291 + def _check_affected_anothers(self, result) -> bool: + """Check if the fix affects the number of lines of another remark.""" + line_index = result['line'] - 1 + target = self.source[line_index] + original_target = self.original_source[line_index] + return target != original_target + def _fix_source(self, results): try: (logical_start, logical_end) = _find_logical(self.source) @@ -557,8 +573,12 @@ def _fix_source(self, results): completed_lines): continue + if self._check_affected_anothers(result): + continue modified_lines = fix(result, logical) else: + if self._check_affected_anothers(result): + continue modified_lines = fix(result) if modified_lines is None: @@ -2045,6 +2065,7 @@ def __init__(self, max_line_length): self._bracket_depth = 0 self._prev_item = None self._prev_prev_item = None + self._in_fstring = False def __repr__(self): return self.emit() @@ -2172,6 +2193,11 @@ def _add_item(self, item, indent_amt): inserted inside of a container or not. """ + if item.is_fstring_start: + self._in_fstring = True + elif self._prev_item and self._prev_item.is_fstring_end: + self._in_fstring = False + if self._prev_item and self._prev_item.is_string and item.is_string: # Place consecutive string literals on separate lines. self._lines.append(self._LineBreak()) @@ -2198,10 +2224,10 @@ def _add_item(self, item, indent_amt): self._lines.append(item) self._prev_item, self._prev_prev_item = item, self._prev_item - if item_text in '([{': + if item_text in '([{' and not self._in_fstring: self._bracket_depth += 1 - elif item_text in '}])': + elif item_text in '}])' and not self._in_fstring: self._bracket_depth -= 1 assert self._bracket_depth >= 0 @@ -2400,6 +2426,18 @@ def is_keyword(self): def is_string(self): return self._atom.token_type == tokenize.STRING + @property + def is_fstring_start(self): + if not IS_SUPPORT_TOKEN_FSTRING: + return False + return self._atom.token_type == tokenize.FSTRING_START + + @property + def is_fstring_end(self): + if not IS_SUPPORT_TOKEN_FSTRING: + return False + return self._atom.token_type == tokenize.FSTRING_END + @property def is_name(self): return self._atom.token_type == tokenize.NAME @@ -3338,13 +3376,20 @@ def multiline_string_lines(source, include_docstrings=False): """ line_numbers = set() previous_token_type = '' + _check_target_tokens = [tokenize.STRING] + if IS_SUPPORT_TOKEN_FSTRING: + _check_target_tokens.extend([ + tokenize.FSTRING_START, + tokenize.FSTRING_MIDDLE, + tokenize.FSTRING_END, + ]) try: for t in generate_tokens(source): token_type = t[0] start_row = t[2][0] end_row = t[3][0] - if token_type == tokenize.STRING and start_row != end_row: + if token_type in _check_target_tokens and start_row != end_row: if ( include_docstrings or previous_token_type != tokenize.INDENT @@ -3931,6 +3976,16 @@ def parse_args(arguments, apply_config=False): 'to the second', ) + original_formatwarning = warnings.formatwarning + warnings.formatwarning = _custom_formatwarning + if args.experimental: + warnings.warn( + "`experimental` option is deprecated and will be " + "removed in a future version.", + DeprecationWarning, + ) + warnings.formatwarning = original_formatwarning + return args diff --git a/test/test_autopep8.py b/test/test_autopep8.py index 64031066..bf73fa08 100755 --- a/test/test_autopep8.py +++ b/test/test_autopep8.py @@ -2256,6 +2256,18 @@ def test_e271_with_multiline(self): with autopep8_context(line) as result: self.assertEqual(fixed, result) + def test_e271_and_w504_with_affects_another_result_line(self): + line = """\ +cm_opts = ([1] + + [d for d in [3,4]]) +""" + fixed = """\ +cm_opts = ([1] + + [d for d in [3,4]]) +""" + with autopep8_context(line, options=["--select=E271,W504"]) as result: + self.assertEqual(fixed, result) + def test_e272(self): line = 'True and False\n' fixed = 'True and False\n' @@ -3948,7 +3960,7 @@ def test_e701_with_escaped_newline(self): with autopep8_context(line) as result: self.assertEqual(fixed, result) - @unittest.skipIf(sys.version_info >= (3, 12), 'not detech in Python3.12+') + @unittest.skipIf(sys.version_info >= (3, 12), 'not detect in Python3.12+') def test_e701_with_escaped_newline_and_spaces(self): line = 'if True: \\ \nprint(True)\n' fixed = 'if True:\n print(True)\n' @@ -4446,7 +4458,7 @@ def test_e731_with_default_arguments(self): with autopep8_context(line, options=['--select=E731']) as result: self.assertEqual(fixed, result) - @unittest.skipIf(sys.version_info >= (3, 12), 'not detech in Python3.12+') + @unittest.skipIf(sys.version_info >= (3, 12), 'not detect in Python3.12+') def test_e901_should_cause_indentation_screw_up(self): line = """\ def tmp(g): @@ -5495,7 +5507,7 @@ def test_help(self): stdout=PIPE) self.assertIn('usage:', p.communicate()[0].decode('utf-8').lower()) - @unittest.skipIf(sys.version_info >= (3, 12), 'not detech in Python3.12+') + @unittest.skipIf(sys.version_info >= (3, 12), 'not detect in Python3.12+') def test_verbose(self): line = 'bad_syntax)' with temporary_file_context(line) as filename: @@ -7236,7 +7248,7 @@ def f(self): with autopep8_context(line, options=['--experimental']) as result: self.assertEqual(fixed, result) - @unittest.skipIf(sys.version_info >= (3, 12), 'not detech in Python3.12+') + @unittest.skipIf(sys.version_info >= (3, 12), 'not detect in Python3.12+') def test_e501_experimental_parsing_dict_with_comments(self): line = """\ self.display['xxxxxxxxxxxx'] = [{'title': _('Library'), #. This is the first comment. @@ -7260,7 +7272,7 @@ def test_e501_experimental_parsing_dict_with_comments(self): with autopep8_context(line, options=['--experimental']) as result: self.assertEqual(fixed, result) - @unittest.skipIf(sys.version_info >= (3, 12), 'not detech in Python3.12+') + @unittest.skipIf(sys.version_info >= (3, 12), 'not detect in Python3.12+') def test_e501_experimental_if_line_over_limit(self): line = """\ if not xxxxxxxxxxxx(aaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccc, dddddddddddddddddddddd): @@ -7333,6 +7345,14 @@ def test_e501_experimental_with_in(self): with autopep8_context(line, options=['--experimental']) as result: self.assertEqual(fixed, result) + @unittest.skipIf(sys.version_info < (3, 12), 'not support in Python3.11 and lower version') + def test_e501_experimental_not_effect_with_fstring(self): + line = """\ +fstring = {"some_key": f"There is a string value inside of an f string, which itself is a dictionary value {s})"} +""" + with autopep8_context(line, options=['--experimental']) as result: + self.assertEqual(line, result) + def fix_e266(source): with autopep8_context(source, options=['--select=E266']) as result: