From 18d13199f3584254962618a94cfbc3751d3a64b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Wed, 21 Oct 2020 22:05:19 +0500 Subject: [PATCH 01/17] Add rules dict, transform interface and dispatcher function. --- unify.py | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/unify.py b/unify.py index 15f0a56..2c64fc1 100755 --- a/unify.py +++ b/unify.py @@ -27,8 +27,10 @@ from __future__ import print_function from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod, abstractproperty import io import os +import re import signal import sys import tokenize @@ -45,6 +47,76 @@ unicode = str +# dict with transform rules +rules = {} + + +class AbstractString: + """Interface to transform strings.""" + __metaclass__ = ABCMeta + + @abstractmethod + def reformat(self): pass + + @abstractproperty + def token(self): pass + + @abstractproperty + def old_token(self): pass + + +class ImmutableString(AbstractString): + """ + Null object. + + Don't transform string. + """ + + def __init__(self, body): + self.body = body + + def reformat(self): pass + + @property + def token(self): + return self.body + + @property + def old_token(self): + return self.body + + +class SimpleString(AbstractString): + """ + String without quote in body. + + Use prefered_quote rule. + """ + + def __init__(self, prefix, quote, body): + self.prefix = prefix + self.quote = quote + self.body = body + self.old_prefix = prefix + self.old_quote = quote + + def reformat(self): + preferred_quote = rules['preferred_quote'] + self.quote = preferred_quote + + @property + def token(self): + return '{prefix}{quote}{body}{quote}'.format( + prefix=self.prefix, quote=self.quote, body=self.body + ) + + @property + def old_token(self): + return '{prefix}{quote}{body}{quote}'.format( + prefix=self.prefix, quote=self.old_quote, body=self.body + ) + + def format_code(source, preferred_quote="'"): """Return source code with quotes unified.""" try: @@ -77,6 +149,30 @@ def _format_code(source, preferred_quote): return untokenize.untokenize(modified_tokens) +def get_string_object(token_type, token_string): + """dispatcher function.""" + if token_type != tokenize.STRING: + return ImmutableString(token_string) + + string_pattern = r'''(?P[rubf]*)(?P['"]{3}|['"])(?P.*)(?P=quote)''' + m = re.match(string_pattern, token_string, re.I | re.S) + + if not m: + return ImmutableString(token_string) + + parsed_string = m.groupdict() + + if parsed_string['quote'] in ('"""', "'''"): + return ImmutableString(token_string) + if all(qt in parsed_string['body'] for qt in ("'", '"')): + # don't transform complicated escape yet + return ImmutableString(token_string) + if any(qt in parsed_string['body'] for qt in ("'", '"')): + # don't transform simple escape yet + return ImmutableString(token_string) + return SimpleString(**parsed_string) + + def unify_quotes(token_string, preferred_quote): """Return string with quotes changed to preferred_quote if possible.""" bad_quote = {'"': "'", @@ -193,6 +289,7 @@ def _main(argv, standard_out, standard_error): args = parser.parse_args(argv[1:]) + rules['preferred_quote'] = args.quote filenames = list(set(args.files)) changes_needed = False failure = False From f6cd7646bc538a8762c3ebd719d4f343fd8d8bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Wed, 21 Oct 2020 22:48:33 +0500 Subject: [PATCH 02/17] test reorganization --- test_unify.py | 247 +++++++++++++++++++------------------------------- 1 file changed, 94 insertions(+), 153 deletions(-) diff --git a/test_unify.py b/test_unify.py index b017fb4..5b5e6c9 100755 --- a/test_unify.py +++ b/test_unify.py @@ -17,210 +17,151 @@ import unify -class TestUnits(unittest.TestCase): +class TestUnitsSimpleString(unittest.TestCase): - def test_unify_quotes(self): - self.assertEqual("'foo'", - unify.unify_quotes('"foo"', - preferred_quote="'")) + def test_preferred_single(self): + result = unify.unify_quotes('"foo"', preferred_quote="'") + self.assertEqual(result, "'foo'") - self.assertEqual('"foo"', - unify.unify_quotes('"foo"', - preferred_quote='"')) + result = unify.unify_quotes('f"foo"', preferred_quote="'") + self.assertEqual(result, "f'foo'") - self.assertEqual('"foo"', - unify.unify_quotes("'foo'", - preferred_quote='"')) + result = unify.unify_quotes('r"foo"', preferred_quote="'") + self.assertEqual(result, "r'foo'") - def test_unify_quotes_should_avoid_some_cases(self): - self.assertEqual('''"foo's"''', - unify.unify_quotes('''"foo's"''', - preferred_quote="'")) + result = unify.unify_quotes('u"foo"', preferred_quote="'") + self.assertEqual(result, "u'foo'") - self.assertEqual('''"""foo"""''', - unify.unify_quotes('''"""foo"""''', - preferred_quote="'")) + result = unify.unify_quotes('b"foo"', preferred_quote="'") + self.assertEqual(result, "b'foo'") - def test_detect_encoding_with_bad_encoding(self): - with temporary_file('# -*- coding: blah -*-\n') as filename: - self.assertEqual('latin-1', - unify.detect_encoding(filename)) - def test_format_code(self): - self.assertEqual("x = 'abc' \\\n'next'\n", - unify.format_code('x = "abc" \\\n"next"\n', - preferred_quote="'")) + def test_preferred_double(self): + result = unify.unify_quotes("'foo'", preferred_quote='"') + self.assertEqual(result, '"foo"') - def test_format_code_with_backslash_in_comment(self): - self.assertEqual("x = 'abc' #\\\n'next'\n", - unify.format_code('x = "abc" #\\\n"next"\n', - preferred_quote="'")) + result = unify.unify_quotes("f'foo'", preferred_quote='"') + self.assertEqual(result, 'f"foo"') - def test_format_code_with_syntax_error(self): - self.assertEqual('foo("abc"\n', - unify.format_code('foo("abc"\n', - preferred_quote="'")) - - -class TestUnitsWithFstrings(unittest.TestCase): - """ Tests for python >= 3.6 fstring handling.""" - - def test_unify_quotes(self): - self.assertEqual("f'foo'", - unify.unify_quotes('f"foo"', - preferred_quote="'")) + result = unify.unify_quotes("r'foo'", preferred_quote='"') + self.assertEqual(result, 'r"foo"') - self.assertEqual('f"foo"', - unify.unify_quotes('f"foo"', - preferred_quote='"')) + result = unify.unify_quotes("u'foo'", preferred_quote='"') + self.assertEqual(result, 'u"foo"') - self.assertEqual('f"foo"', - unify.unify_quotes("f'foo'", - preferred_quote='"')) + result = unify.unify_quotes("b'foo'", preferred_quote='"') + self.assertEqual(result, 'b"foo"') - def test_unify_quotes_should_avoid_some_cases(self): - self.assertEqual('''f"foo's"''', - unify.unify_quotes('''f"foo's"''', - preferred_quote="'")) - - self.assertEqual('''f"""foo"""''', - unify.unify_quotes('''f"""foo"""''', - preferred_quote="'")) - - def test_format_code(self): - self.assertEqual("x = f'abc' \\\nf'next'\n", - unify.format_code('x = f"abc" \\\nf"next"\n', - preferred_quote="'")) + def test_keep(self): + result = unify.unify_quotes("'foo'", preferred_quote="'") + self.assertEqual(result, "'foo'") - def test_format_code_with_backslash_in_comment(self): - self.assertEqual("x = f'abc' #\\\nf'next'\n", - unify.format_code('x = f"abc" #\\\nf"next"\n', - preferred_quote="'")) + result = unify.unify_quotes('"foo"', preferred_quote='"') + self.assertEqual(result, '"foo"') - def test_format_code_with_syntax_error(self): - self.assertEqual('foo(f"abc"\n', - unify.format_code('foo(f"abc"\n', - preferred_quote="'")) +class TestUnitsSimpleQuotedString(unittest.TestCase): -class TestUnitsWithRawStrings(unittest.TestCase): - """Test for r-prefix raw string handling.""" + def test_single_in_body(self): + result = unify.unify_quotes('''"foo's"''', preferred_quote="'") + self.assertEqual(result, '''"foo's"''') - def test_unify_quotes(self): - self.assertEqual("r'foo'", - unify.unify_quotes('r"foo"', - preferred_quote="'")) + result = unify.unify_quotes('''f"foo's"''', preferred_quote="'") + self.assertEqual(result, '''f"foo's"''') - self.assertEqual('r"foo"', - unify.unify_quotes('r"foo"', - preferred_quote='"')) + result = unify.unify_quotes('''r"foo's"''', preferred_quote="'") + self.assertEqual(result, '''r"foo's"''') - self.assertEqual('r"foo"', - unify.unify_quotes("r'foo'", - preferred_quote='"')) + result = unify.unify_quotes('''u"foo's"''', preferred_quote="'") + self.assertEqual(result, '''u"foo's"''') - def test_unify_quotes_should_avoid_some_cases(self): - self.assertEqual('''r"foo's"''', - unify.unify_quotes('''r"foo's"''', - preferred_quote="'")) + result = unify.unify_quotes('''b"foo's"''', preferred_quote="'") + self.assertEqual(result, '''b"foo's"''') - self.assertEqual('''r"""\\t"""''', - unify.unify_quotes('''r"""\\t"""''', - preferred_quote="'")) - def test_format_code(self): - self.assertEqual("x = r'abc' \\\nr'next'\n", - unify.format_code('x = r"abc" \\\nr"next"\n', - preferred_quote="'")) +class TestUnitsTripleQuote(unittest.TestCase): - def test_format_code_with_backslash_in_comment(self): - self.assertEqual("x = r'abc' #\\\nr'next'\n", - unify.format_code('x = r"abc" #\\\nr"next"\n', - preferred_quote="'")) + def test_no_change(self): + result = unify.unify_quotes('''"""foo"""''', preferred_quote="'") + self.assertEqual(result, '''"""foo"""''') - def test_format_code_with_syntax_error(self): - self.assertEqual('foo(r"Tabs \t, new lines \n."\n', - unify.format_code('foo(r"Tabs \t, new lines \n."\n', - preferred_quote="'")) + result = unify.unify_quotes('''f"""foo"""''', preferred_quote="'") + self.assertEqual(result, '''f"""foo"""''') + result = unify.unify_quotes('''r"""\\t"""''', preferred_quote="'") + self.assertEqual(result, '''r"""\\t"""''') -class TestUnitsWithUnicodeStrings(unittest.TestCase): - """Test for u-prefix unicode string handling.""" + result = unify.unify_quotes('''u"""foo"""''', preferred_quote="'") + self.assertEqual(result, '''u"""foo"""''') - def test_unify_quotes(self): - self.assertEqual("u'foo'", - unify.unify_quotes('u"foo"', - preferred_quote="'")) + result = unify.unify_quotes('''b"""foo"""''', preferred_quote="'") + self.assertEqual(result, '''b"""foo"""''') - self.assertEqual('u"foo"', - unify.unify_quotes('u"foo"', - preferred_quote='"')) +class TestUnitsCode(unittest.TestCase): - self.assertEqual('u"foo"', - unify.unify_quotes("u'foo'", - preferred_quote='"')) + def test_detect_encoding_with_bad_encoding(self): + with temporary_file('# -*- coding: blah -*-\n') as filename: + self.assertEqual('latin-1', + unify.detect_encoding(filename)) - def test_unify_quotes_should_avoid_some_cases(self): - self.assertEqual('''u"foo's"''', - unify.unify_quotes('''u"foo's"''', - preferred_quote="'")) + def test_format_code(self): + self.assertEqual("x = 'abc' \\\n'next'\n", + unify.format_code('x = "abc" \\\n"next"\n', + preferred_quote="'")) - self.assertEqual('''u"""foo"""''', - unify.unify_quotes('''u"""foo"""''', - preferred_quote="'")) + self.assertEqual("x = f'abc' \\\nf'next'\n", + unify.format_code('x = f"abc" \\\nf"next"\n', + preferred_quote="'")) - def test_format_code(self): self.assertEqual("x = u'abc' \\\nu'next'\n", unify.format_code('x = u"abc" \\\nu"next"\n', preferred_quote="'")) + self.assertEqual("x = b'abc' \\\nb'next'\n", + unify.format_code('x = b"abc" \\\nb"next"\n', + preferred_quote="'")) def test_format_code_with_backslash_in_comment(self): - self.assertEqual("x = u'abc' #\\\nu'next'\n", - unify.format_code('x = u"abc" #\\\nu"next"\n', + self.assertEqual("x = 'abc' #\\\n'next'\n", + unify.format_code('x = "abc" #\\\n"next"\n', preferred_quote="'")) - def test_format_code_with_syntax_error(self): - self.assertEqual('foo(u"abc"\n', - unify.format_code('foo(u"abc"\n', + self.assertEqual("x = f'abc' #\\\nf'next'\n", + unify.format_code('x = f"abc" #\\\nf"next"\n', preferred_quote="'")) + self.assertEqual("x = r'abc' #\\\nr'next'\n", + unify.format_code('x = r"abc" #\\\nr"next"\n', + preferred_quote="'")) -class TestUnitsWithByteStrings(unittest.TestCase): - """ Tests for python3 byte string handling.""" - - def test_unify_quotes(self): - self.assertEqual("b'foo'", - unify.unify_quotes('b"foo"', - preferred_quote="'")) + self.assertEqual("x = r'abc' \\\nr'next'\n", + unify.format_code('x = r"abc" \\\nr"next"\n', + preferred_quote="'")) - self.assertEqual('b"foo"', - unify.unify_quotes('b"foo"', - preferred_quote='"')) + self.assertEqual("x = u'abc' #\\\nu'next'\n", + unify.format_code('x = u"abc" #\\\nu"next"\n', + preferred_quote="'")) - self.assertEqual('b"foo"', - unify.unify_quotes("b'foo'", - preferred_quote='"')) + self.assertEqual("x = b'abc' #\\\nb'next'\n", + unify.format_code('x = b"abc" #\\\nb"next"\n', + preferred_quote="'")) - def test_unify_quotes_should_avoid_some_cases(self): - self.assertEqual('''b"foo's"''', - unify.unify_quotes('''b"foo's"''', - preferred_quote="'")) + def test_format_code_with_syntax_error(self): + self.assertEqual('foo("abc"\n', + unify.format_code('foo("abc"\n', + preferred_quote="'")) - self.assertEqual('''b"""foo"""''', - unify.unify_quotes('''b"""foo"""''', - preferred_quote="'")) + self.assertEqual('foo(f"abc"\n', + unify.format_code('foo(f"abc"\n', + preferred_quote="'")) - def test_format_code(self): - self.assertEqual("x = b'abc' \\\nb'next'\n", - unify.format_code('x = b"abc" \\\nb"next"\n', + self.assertEqual('foo(r"Tabs \t, new lines \n."\n', + unify.format_code('foo(r"Tabs \t, new lines \n."\n', preferred_quote="'")) - def test_format_code_with_backslash_in_comment(self): - self.assertEqual("x = b'abc' #\\\nb'next'\n", - unify.format_code('x = b"abc" #\\\nb"next"\n', + self.assertEqual('foo(u"abc"\n', + unify.format_code('foo(u"abc"\n', preferred_quote="'")) - def test_format_code_with_syntax_error(self): self.assertEqual('foo(b"abc"\n', unify.format_code('foo(b"abc"\n', preferred_quote="'")) From b83e3d21b203372b3719f82e5668f78abacc3ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Wed, 21 Oct 2020 22:59:26 +0500 Subject: [PATCH 03/17] Formatting use rules. --- test_unify.py | 109 ++++++++++++++++++++++++++------------------------ unify.py | 16 ++++---- 2 files changed, 63 insertions(+), 62 deletions(-) diff --git a/test_unify.py b/test_unify.py index 5b5e6c9..8ba06fd 100755 --- a/test_unify.py +++ b/test_unify.py @@ -20,81 +20,92 @@ class TestUnitsSimpleString(unittest.TestCase): def test_preferred_single(self): - result = unify.unify_quotes('"foo"', preferred_quote="'") + unify.rules['preferred_quote'] = "'" + + result = unify.unify_quotes('"foo"') self.assertEqual(result, "'foo'") - result = unify.unify_quotes('f"foo"', preferred_quote="'") + result = unify.unify_quotes('f"foo"') self.assertEqual(result, "f'foo'") - result = unify.unify_quotes('r"foo"', preferred_quote="'") + result = unify.unify_quotes('r"foo"') self.assertEqual(result, "r'foo'") - result = unify.unify_quotes('u"foo"', preferred_quote="'") + result = unify.unify_quotes('u"foo"') self.assertEqual(result, "u'foo'") - result = unify.unify_quotes('b"foo"', preferred_quote="'") + result = unify.unify_quotes('b"foo"') self.assertEqual(result, "b'foo'") def test_preferred_double(self): - result = unify.unify_quotes("'foo'", preferred_quote='"') + unify.rules['preferred_quote'] = '"' + + result = unify.unify_quotes("'foo'") self.assertEqual(result, '"foo"') - result = unify.unify_quotes("f'foo'", preferred_quote='"') + result = unify.unify_quotes("f'foo'") self.assertEqual(result, 'f"foo"') - result = unify.unify_quotes("r'foo'", preferred_quote='"') + result = unify.unify_quotes("r'foo'") self.assertEqual(result, 'r"foo"') - result = unify.unify_quotes("u'foo'", preferred_quote='"') + result = unify.unify_quotes("u'foo'") self.assertEqual(result, 'u"foo"') - result = unify.unify_quotes("b'foo'", preferred_quote='"') + result = unify.unify_quotes("b'foo'") self.assertEqual(result, 'b"foo"') - def test_keep(self): - result = unify.unify_quotes("'foo'", preferred_quote="'") + def test_keep_single(self): + unify.rules['preferred_quote'] = "'" + result = unify.unify_quotes("'foo'") self.assertEqual(result, "'foo'") - result = unify.unify_quotes('"foo"', preferred_quote='"') + def test_keep_double(self): + unify.rules['preferred_quote'] = '"' + result = unify.unify_quotes('"foo"') self.assertEqual(result, '"foo"') class TestUnitsSimpleQuotedString(unittest.TestCase): def test_single_in_body(self): - result = unify.unify_quotes('''"foo's"''', preferred_quote="'") + unify.rules['preferred_quote'] = "'" + + result = unify.unify_quotes('''"foo's"''') self.assertEqual(result, '''"foo's"''') - result = unify.unify_quotes('''f"foo's"''', preferred_quote="'") + result = unify.unify_quotes('''f"foo's"''') self.assertEqual(result, '''f"foo's"''') - result = unify.unify_quotes('''r"foo's"''', preferred_quote="'") + result = unify.unify_quotes('''r"foo's"''') self.assertEqual(result, '''r"foo's"''') - result = unify.unify_quotes('''u"foo's"''', preferred_quote="'") + result = unify.unify_quotes('''u"foo's"''') self.assertEqual(result, '''u"foo's"''') - result = unify.unify_quotes('''b"foo's"''', preferred_quote="'") + result = unify.unify_quotes('''b"foo's"''') self.assertEqual(result, '''b"foo's"''') class TestUnitsTripleQuote(unittest.TestCase): def test_no_change(self): - result = unify.unify_quotes('''"""foo"""''', preferred_quote="'") + unify.rules['preferred_quote'] = "'" + + result = unify.unify_quotes('''"""foo"""''') self.assertEqual(result, '''"""foo"""''') - result = unify.unify_quotes('''f"""foo"""''', preferred_quote="'") + result = unify.unify_quotes('''f"""foo"""''') self.assertEqual(result, '''f"""foo"""''') - result = unify.unify_quotes('''r"""\\t"""''', preferred_quote="'") + result = unify.unify_quotes('''r"""\\t"""''') self.assertEqual(result, '''r"""\\t"""''') - result = unify.unify_quotes('''u"""foo"""''', preferred_quote="'") + result = unify.unify_quotes('''u"""foo"""''') self.assertEqual(result, '''u"""foo"""''') - result = unify.unify_quotes('''b"""foo"""''', preferred_quote="'") + result = unify.unify_quotes('''b"""foo"""''') self.assertEqual(result, '''b"""foo"""''') class TestUnitsCode(unittest.TestCase): @@ -105,66 +116,58 @@ def test_detect_encoding_with_bad_encoding(self): unify.detect_encoding(filename)) def test_format_code(self): + unify.rules['preferred_quote'] = "'" + self.assertEqual("x = 'abc' \\\n'next'\n", - unify.format_code('x = "abc" \\\n"next"\n', - preferred_quote="'")) + unify.format_code('x = "abc" \\\n"next"\n')) self.assertEqual("x = f'abc' \\\nf'next'\n", - unify.format_code('x = f"abc" \\\nf"next"\n', - preferred_quote="'")) + unify.format_code('x = f"abc" \\\nf"next"\n')) self.assertEqual("x = u'abc' \\\nu'next'\n", - unify.format_code('x = u"abc" \\\nu"next"\n', - preferred_quote="'")) + unify.format_code('x = u"abc" \\\nu"next"\n')) self.assertEqual("x = b'abc' \\\nb'next'\n", - unify.format_code('x = b"abc" \\\nb"next"\n', - preferred_quote="'")) + unify.format_code('x = b"abc" \\\nb"next"\n')) + def test_format_code_with_backslash_in_comment(self): + unify.rules['preferred_quote'] = "'" + self.assertEqual("x = 'abc' #\\\n'next'\n", - unify.format_code('x = "abc" #\\\n"next"\n', - preferred_quote="'")) + unify.format_code('x = "abc" #\\\n"next"\n')) self.assertEqual("x = f'abc' #\\\nf'next'\n", - unify.format_code('x = f"abc" #\\\nf"next"\n', - preferred_quote="'")) + unify.format_code('x = f"abc" #\\\nf"next"\n')) self.assertEqual("x = r'abc' #\\\nr'next'\n", - unify.format_code('x = r"abc" #\\\nr"next"\n', - preferred_quote="'")) + unify.format_code('x = r"abc" #\\\nr"next"\n')) self.assertEqual("x = r'abc' \\\nr'next'\n", - unify.format_code('x = r"abc" \\\nr"next"\n', - preferred_quote="'")) + unify.format_code('x = r"abc" \\\nr"next"\n')) self.assertEqual("x = u'abc' #\\\nu'next'\n", - unify.format_code('x = u"abc" #\\\nu"next"\n', - preferred_quote="'")) + unify.format_code('x = u"abc" #\\\nu"next"\n')) self.assertEqual("x = b'abc' #\\\nb'next'\n", - unify.format_code('x = b"abc" #\\\nb"next"\n', - preferred_quote="'")) + unify.format_code('x = b"abc" #\\\nb"next"\n')) def test_format_code_with_syntax_error(self): + unify.rules['preferred_quote'] = "'" + self.assertEqual('foo("abc"\n', - unify.format_code('foo("abc"\n', - preferred_quote="'")) + unify.format_code('foo("abc"\n')) self.assertEqual('foo(f"abc"\n', - unify.format_code('foo(f"abc"\n', - preferred_quote="'")) + unify.format_code('foo(f"abc"\n')) self.assertEqual('foo(r"Tabs \t, new lines \n."\n', - unify.format_code('foo(r"Tabs \t, new lines \n."\n', - preferred_quote="'")) + unify.format_code('foo(r"Tabs \t, new lines \n."\n')) self.assertEqual('foo(u"abc"\n', - unify.format_code('foo(u"abc"\n', - preferred_quote="'")) + unify.format_code('foo(u"abc"\n')) self.assertEqual('foo(b"abc"\n', - unify.format_code('foo(b"abc"\n', - preferred_quote="'")) + unify.format_code('foo(b"abc"\n')) class TestSystem(unittest.TestCase): diff --git a/unify.py b/unify.py index 2c64fc1..4613001 100755 --- a/unify.py +++ b/unify.py @@ -117,15 +117,15 @@ def old_token(self): ) -def format_code(source, preferred_quote="'"): +def format_code(source): """Return source code with quotes unified.""" try: - return _format_code(source, preferred_quote) + return _format_code(source) except (tokenize.TokenError, IndentationError): return source -def _format_code(source, preferred_quote): +def _format_code(source): """Return source code with quotes unified.""" if not source: return source @@ -140,8 +140,7 @@ def _format_code(source, preferred_quote): line) in tokenize.generate_tokens(sio.readline): if token_type == tokenize.STRING: - token_string = unify_quotes(token_string, - preferred_quote=preferred_quote) + token_string = unify_quotes(token_string) modified_tokens.append( (token_type, token_string, start, end, line)) @@ -173,8 +172,9 @@ def get_string_object(token_type, token_string): return SimpleString(**parsed_string) -def unify_quotes(token_string, preferred_quote): +def unify_quotes(token_string): """Return string with quotes changed to preferred_quote if possible.""" + preferred_quote = rules['preferred_quote'] bad_quote = {'"': "'", "'": '"'}[preferred_quote] @@ -240,9 +240,7 @@ def format_file(filename, args, standard_out): encoding = detect_encoding(filename) with open_with_encoding(filename, encoding=encoding) as input_file: source = input_file.read() - formatted_source = format_code( - source, - preferred_quote=args.quote) + formatted_source = format_code(source) if source != formatted_source: if args.in_place: From 1d70405826947fc0d9e1ad491077626db2add1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Wed, 21 Oct 2020 23:10:48 +0500 Subject: [PATCH 04/17] Edit with SimpleString. --- test_unify.py | 44 ++++++++++++++++++++++---------------------- unify.py | 43 ++++--------------------------------------- 2 files changed, 26 insertions(+), 61 deletions(-) diff --git a/test_unify.py b/test_unify.py index 8ba06fd..41cd1e0 100755 --- a/test_unify.py +++ b/test_unify.py @@ -22,48 +22,48 @@ class TestUnitsSimpleString(unittest.TestCase): def test_preferred_single(self): unify.rules['preferred_quote'] = "'" - result = unify.unify_quotes('"foo"') + result = unify.format_code('"foo"') self.assertEqual(result, "'foo'") - result = unify.unify_quotes('f"foo"') + result = unify.format_code('f"foo"') self.assertEqual(result, "f'foo'") - result = unify.unify_quotes('r"foo"') + result = unify.format_code('r"foo"') self.assertEqual(result, "r'foo'") - result = unify.unify_quotes('u"foo"') + result = unify.format_code('u"foo"') self.assertEqual(result, "u'foo'") - result = unify.unify_quotes('b"foo"') + result = unify.format_code('b"foo"') self.assertEqual(result, "b'foo'") def test_preferred_double(self): unify.rules['preferred_quote'] = '"' - result = unify.unify_quotes("'foo'") + result = unify.format_code("'foo'") self.assertEqual(result, '"foo"') - result = unify.unify_quotes("f'foo'") + result = unify.format_code("f'foo'") self.assertEqual(result, 'f"foo"') - result = unify.unify_quotes("r'foo'") + result = unify.format_code("r'foo'") self.assertEqual(result, 'r"foo"') - result = unify.unify_quotes("u'foo'") + result = unify.format_code("u'foo'") self.assertEqual(result, 'u"foo"') - result = unify.unify_quotes("b'foo'") + result = unify.format_code("b'foo'") self.assertEqual(result, 'b"foo"') def test_keep_single(self): unify.rules['preferred_quote'] = "'" - result = unify.unify_quotes("'foo'") + result = unify.format_code("'foo'") self.assertEqual(result, "'foo'") def test_keep_double(self): unify.rules['preferred_quote'] = '"' - result = unify.unify_quotes('"foo"') + result = unify.format_code('"foo"') self.assertEqual(result, '"foo"') @@ -72,19 +72,19 @@ class TestUnitsSimpleQuotedString(unittest.TestCase): def test_single_in_body(self): unify.rules['preferred_quote'] = "'" - result = unify.unify_quotes('''"foo's"''') + result = unify.format_code('''"foo's"''') self.assertEqual(result, '''"foo's"''') - result = unify.unify_quotes('''f"foo's"''') + result = unify.format_code('''f"foo's"''') self.assertEqual(result, '''f"foo's"''') - result = unify.unify_quotes('''r"foo's"''') + result = unify.format_code('''r"foo's"''') self.assertEqual(result, '''r"foo's"''') - result = unify.unify_quotes('''u"foo's"''') + result = unify.format_code('''u"foo's"''') self.assertEqual(result, '''u"foo's"''') - result = unify.unify_quotes('''b"foo's"''') + result = unify.format_code('''b"foo's"''') self.assertEqual(result, '''b"foo's"''') @@ -93,19 +93,19 @@ class TestUnitsTripleQuote(unittest.TestCase): def test_no_change(self): unify.rules['preferred_quote'] = "'" - result = unify.unify_quotes('''"""foo"""''') + result = unify.format_code('''"""foo"""''') self.assertEqual(result, '''"""foo"""''') - result = unify.unify_quotes('''f"""foo"""''') + result = unify.format_code('''f"""foo"""''') self.assertEqual(result, '''f"""foo"""''') - result = unify.unify_quotes('''r"""\\t"""''') + result = unify.format_code('''r"""\\t"""''') self.assertEqual(result, '''r"""\\t"""''') - result = unify.unify_quotes('''u"""foo"""''') + result = unify.format_code('''u"""foo"""''') self.assertEqual(result, '''u"""foo"""''') - result = unify.unify_quotes('''b"""foo"""''') + result = unify.format_code('''b"""foo"""''') self.assertEqual(result, '''b"""foo"""''') class TestUnitsCode(unittest.TestCase): diff --git a/unify.py b/unify.py index 4613001..73f7bba 100755 --- a/unify.py +++ b/unify.py @@ -139,8 +139,9 @@ def _format_code(source): end, line) in tokenize.generate_tokens(sio.readline): - if token_type == tokenize.STRING: - token_string = unify_quotes(token_string) + editable_string = get_editable_string(token_type, token_string) + editable_string.reformat() + token_string = editable_string.token modified_tokens.append( (token_type, token_string, start, end, line)) @@ -148,7 +149,7 @@ def _format_code(source): return untokenize.untokenize(modified_tokens) -def get_string_object(token_type, token_string): +def get_editable_string(token_type, token_string): """dispatcher function.""" if token_type != tokenize.STRING: return ImmutableString(token_string) @@ -172,42 +173,6 @@ def get_string_object(token_type, token_string): return SimpleString(**parsed_string) -def unify_quotes(token_string): - """Return string with quotes changed to preferred_quote if possible.""" - preferred_quote = rules['preferred_quote'] - bad_quote = {'"': "'", - "'": '"'}[preferred_quote] - - allowed_starts = { - '': bad_quote, - 'f': 'f' + bad_quote, - 'r': 'r' + bad_quote, - 'u': 'u' + bad_quote, - 'b': 'b' + bad_quote - } - - if not any(token_string.startswith(start) - for start in allowed_starts.values()): - return token_string - - if token_string.count(bad_quote) != 2: - return token_string - - if preferred_quote in token_string: - return token_string - - assert token_string.endswith(bad_quote) - assert len(token_string) >= 2 - for prefix, start in allowed_starts.items(): - if token_string.startswith(start): - chars_to_strip_from_front = len(start) - return '{prefix}{preferred_quote}{token}{preferred_quote}'.format( - prefix=prefix, - preferred_quote=preferred_quote, - token=token_string[chars_to_strip_from_front:-1] - ) - - def open_with_encoding(filename, encoding, mode='r'): """Return opened file with a specific encoding.""" return io.open(filename, mode=mode, encoding=encoding, From 99b4d26fe82dfc7f5af6e70d866848bd50b2336a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Thu, 22 Oct 2020 00:05:21 +0500 Subject: [PATCH 05/17] Add simple escape handling. --- test_unify.py | 55 +++++++++++++++++++++++++++------ unify.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 126 insertions(+), 13 deletions(-) diff --git a/test_unify.py b/test_unify.py index 41cd1e0..e1d7650 100755 --- a/test_unify.py +++ b/test_unify.py @@ -69,23 +69,60 @@ def test_keep_double(self): class TestUnitsSimpleQuotedString(unittest.TestCase): - def test_single_in_body(self): + def test_opposite(self): unify.rules['preferred_quote'] = "'" + unify.rules['escape_simple'] = 'opposite' result = unify.format_code('''"foo's"''') self.assertEqual(result, '''"foo's"''') - result = unify.format_code('''f"foo's"''') - self.assertEqual(result, '''f"foo's"''') + result = unify.format_code("""'foo"s'""") + self.assertEqual(result, """'foo"s'""") - result = unify.format_code('''r"foo's"''') - self.assertEqual(result, '''r"foo's"''') + result = unify.format_code('''"foo\\"s"''') + self.assertEqual(result, """'foo"s'""") - result = unify.format_code('''u"foo's"''') - self.assertEqual(result, '''u"foo's"''') + result = unify.format_code("""'foo\\'s'""") + self.assertEqual(result, '''"foo's"''') + + def test_backslash(self): + unify.rules['preferred_quote'] = "'" + unify.rules['escape_simple'] = 'backslash' + + result = unify.format_code('''"foo's"''') + self.assertEqual(result, """'foo\\'s'""") + + result = unify.format_code("""'foo"s'""") + self.assertEqual(result, """'foo"s'""") + + result = unify.format_code('''"foo\\"s"''') + self.assertEqual(result, """'foo"s'""") + + result = unify.format_code("""'foo\\'s'""") + self.assertEqual(result, """'foo\\'s'""") + + def test_keep_unformatted(self): + unify.rules['preferred_quote'] = "'" + unify.rules['escape_simple'] = 'opposite' + + result = unify.format_code('''f"foo's{some_var}"''') + self.assertEqual(result, '''f"foo's{some_var}"''') + + result = unify.format_code("""r'foo\\'s'""") + self.assertEqual(result, """r'foo\\'s'""") + + def test_backslash_train(self): + unify.rules['preferred_quote'] = "'" + unify.rules['escape_simple'] = 'opposite' + + result = unify.format_code('''"a'b\\'c\\\\'d\\\\\\'e\\\\\\\\'f"''') + self.assertEqual(result, '''"a'b'c\\\\'d\\\\'e\\\\\\\\'f"''') + + result = unify.format_code('''"\\'a"''') + self.assertEqual(result, '''"'a"''') - result = unify.format_code('''b"foo's"''') - self.assertEqual(result, '''b"foo's"''') + result = unify.format_code('''"\\\\'a"''') + self.assertEqual(result, '''"\\\\'a"''') class TestUnitsTripleQuote(unittest.TestCase): diff --git a/unify.py b/unify.py index 73f7bba..97695a7 100755 --- a/unify.py +++ b/unify.py @@ -113,10 +113,69 @@ def token(self): @property def old_token(self): return '{prefix}{quote}{body}{quote}'.format( - prefix=self.prefix, quote=self.old_quote, body=self.body + prefix=self.old_prefix, quote=self.old_quote, body=self.body ) +class SimpleEscapeString(AbstractString): + """ + String with one type of quote in body. + + Use escape_simple and preferred_quote rules. + """ + OPPOSITE_QUOTE = {"'": '"', '"': "'"} + + def __init__(self, prefix, quote, body): + self.prefix = prefix + self.quote = quote + self.body = body + self.old_prefix = prefix + self.old_quote = quote + self.old_body = body + + def reformat(self): + preferred_quote = rules['preferred_quote'] + escape_simple = rules['escape_simple'] + quote_in_body = "'" if "'" in self.body else '"' + + if escape_simple == 'ignore': return + + body = drop_escape_backslash(self.body) + if escape_simple == 'opposite': + self.quote = self.OPPOSITE_QUOTE[quote_in_body] + else: + self.quote = preferred_quote + if preferred_quote == quote_in_body: + body = body.replace(quote_in_body, '\\' + quote_in_body) + self.body = body + + @property + def token(self): + return '{prefix}{quote}{body}{quote}'.format( + prefix=self.prefix, quote=self.quote, body=self.body + ) + + @property + def old_token(self): + return '{prefix}{quote}{body}{quote}'.format( + prefix=self.old_prefix, quote=self.old_quote, body=self.old_body + ) + + +def drop_escape_backslash(body): + bs_pattern = '(\\\\+[\'"])' + splitted_body = re.split(bs_pattern, body) + + def _drop_escape_bs(string): + if string.startswith('\\') and len(string) % 2 == 0: + string = string[1:] + return string + + splitted_body = [_drop_escape_bs(chunk) for chunk in splitted_body] + body = ''.join(splitted_body) + return body + + def format_code(source): """Return source code with quotes unified.""" try: @@ -164,13 +223,26 @@ def get_editable_string(token_type, token_string): if parsed_string['quote'] in ('"""', "'''"): return ImmutableString(token_string) + if all(qt not in parsed_string['body'] for qt in ("'", '"')): + return SimpleString(**parsed_string) + if 'r' in parsed_string['prefix'].lower(): + # don't transform raw string since can't use backslash + # as escape char + return ImmutableString(token_string) if all(qt in parsed_string['body'] for qt in ("'", '"')): # don't transform complicated escape yet return ImmutableString(token_string) if any(qt in parsed_string['body'] for qt in ("'", '"')): - # don't transform simple escape yet - return ImmutableString(token_string) - return SimpleString(**parsed_string) + if 'f' in parsed_string['prefix'].lower(): + if any(br in parsed_string['body'] for br in '{}'): + # don't transform f-string since can't use + # backslash in bracket area + # need special handling - not implemented yet + return ImmutableString(token_string) + # if don't have brackets we can treat this case as normal string + return SimpleEscapeString(**parsed_string) + return SimpleEscapeString(**parsed_string) + return ImmutableString(token_string) def open_with_encoding(filename, encoding, mode='r'): @@ -245,6 +317,9 @@ def _main(argv, standard_out, standard_error): help='drill down directories recursively') parser.add_argument('--quote', help='preferred quote', choices=["'", '"'], default="'") + parser.add_argument('--escape-simple', help='simple escape strategy', + choices=['opposite', 'backslash', 'ignore'], + default='opposite') parser.add_argument('--version', action='version', version='%(prog)s ' + __version__) parser.add_argument('files', nargs='+', @@ -253,6 +328,7 @@ def _main(argv, standard_out, standard_error): args = parser.parse_args(argv[1:]) rules['preferred_quote'] = args.quote + rules['escape_simple'] = args.escape_simple filenames = list(set(args.files)) changes_needed = False failure = False From 959ff840edc19d18968051f386e4f60f945945a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Thu, 22 Oct 2020 10:12:09 +0500 Subject: [PATCH 06/17] Add stub SimpleEscapeFstring. --- unify.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/unify.py b/unify.py index 97695a7..9f842ce 100755 --- a/unify.py +++ b/unify.py @@ -175,6 +175,23 @@ def _drop_escape_bs(string): body = ''.join(splitted_body) return body +class SimpleEscapeFstring(SimpleEscapeString): + """ + F-string with one type of quote in body. + + Not fully implemented. + Use escape_simple and preferred_quote rules. + """ + + def reformat(self): + if any(br in self.body for br in '{}'): + # don't transform since can't use backslashes in bracket area + # TODO add body parsing and handle this case + return + + # can treat this case as simple escape + super().reformat() + def format_code(source): """Return source code with quotes unified.""" @@ -234,13 +251,7 @@ def get_editable_string(token_type, token_string): return ImmutableString(token_string) if any(qt in parsed_string['body'] for qt in ("'", '"')): if 'f' in parsed_string['prefix'].lower(): - if any(br in parsed_string['body'] for br in '{}'): - # don't transform f-string since can't use - # backslash in bracket area - # need special handling - not implemented yet - return ImmutableString(token_string) - # if don't have brackets we can treat this case as normal string - return SimpleEscapeString(**parsed_string) + return SimpleEscapeFstring(**parsed_string) return SimpleEscapeString(**parsed_string) return ImmutableString(token_string) From 555506af47b2a3fa5362f401bd7c3885dbe6e413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Thu, 22 Oct 2020 15:17:32 +0500 Subject: [PATCH 07/17] Change help message. --- unify.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/unify.py b/unify.py index 9f842ce..24734d7 100755 --- a/unify.py +++ b/unify.py @@ -328,7 +328,8 @@ def _main(argv, standard_out, standard_error): help='drill down directories recursively') parser.add_argument('--quote', help='preferred quote', choices=["'", '"'], default="'") - parser.add_argument('--escape-simple', help='simple escape strategy', + parser.add_argument('--escape-simple', + help='escape strategy if string has one type of quote', choices=['opposite', 'backslash', 'ignore'], default='opposite') parser.add_argument('--version', action='version', From e391e56a549ce50355688d83b099d03c7dfaabd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Tue, 27 Oct 2020 10:05:33 +0500 Subject: [PATCH 08/17] style fixes. --- test_unify.py | 132 ++++++++++++++++++++++++++------------------------ unify.py | 17 ++++--- 2 files changed, 79 insertions(+), 70 deletions(-) diff --git a/test_unify.py b/test_unify.py index e1d7650..889df60 100755 --- a/test_unify.py +++ b/test_unify.py @@ -7,6 +7,7 @@ import contextlib import io import tempfile +from textwrap import dedent try: # Python 2.6 @@ -37,7 +38,6 @@ def test_preferred_single(self): result = unify.format_code('b"foo"') self.assertEqual(result, "b'foo'") - def test_preferred_double(self): unify.rules['preferred_quote'] = '"' @@ -145,12 +145,12 @@ def test_no_change(self): result = unify.format_code('''b"""foo"""''') self.assertEqual(result, '''b"""foo"""''') + class TestUnitsCode(unittest.TestCase): def test_detect_encoding_with_bad_encoding(self): with temporary_file('# -*- coding: blah -*-\n') as filename: - self.assertEqual('latin-1', - unify.detect_encoding(filename)) + self.assertEqual('latin-1', unify.detect_encoding(filename)) def test_format_code(self): unify.rules['preferred_quote'] = "'" @@ -210,42 +210,46 @@ def test_format_code_with_syntax_error(self): class TestSystem(unittest.TestCase): def test_diff(self): - with temporary_file('''\ -if True: - x = "abc" -''') as filename: + with temporary_file(dedent('''\ + if True: + x = "abc" + ''')) as filename: output_file = io.StringIO() self.assertEqual( unify._main(argv=['my_fake_program', filename], standard_out=output_file, standard_error=None), - None, - ) - self.assertEqual('''\ -@@ -1,2 +1,2 @@ - if True: -- x = "abc" -+ x = 'abc' -''', '\n'.join(output_file.getvalue().split('\n')[2:])) + None) + + self.assertEqual( + dedent('''\ + @@ -1,2 +1,2 @@ + if True: + - x = "abc" + + x = 'abc' + '''), + '\n'.join(output_file.getvalue().split('\n')[2:])) def test_check_only(self): - with temporary_file('''\ -if True: - x = "abc" -''') as filename: + with temporary_file(dedent('''\ + if True: + x = "abc" + ''')) as filename: output_file = io.StringIO() self.assertEqual( unify._main(argv=['my_fake_program', '--check-only', filename], standard_out=output_file, standard_error=None), - 1, - ) - self.assertEqual('''\ -@@ -1,2 +1,2 @@ - if True: -- x = "abc" -+ x = 'abc' -''', '\n'.join(output_file.getvalue().split('\n')[2:])) + 1) + + self.assertEqual( + dedent('''\ + @@ -1,2 +1,2 @@ + if True: + - x = "abc" + + x = 'abc' + '''), + '\n'.join(output_file.getvalue().split('\n')[2:])) def test_diff_with_empty_file(self): with temporary_file('') as filename: @@ -253,9 +257,7 @@ def test_diff_with_empty_file(self): unify._main(argv=['my_fake_program', filename], standard_out=output_file, standard_error=None) - self.assertEqual( - '', - output_file.getvalue()) + self.assertEqual('', output_file.getvalue()) def test_diff_with_missing_file(self): output_file = io.StringIO() @@ -263,36 +265,38 @@ def test_diff_with_missing_file(self): self.assertEqual( 1, - unify._main(argv=['my_fake_program', - '/non_existent_file_92394492929'], - standard_out=None, - standard_error=output_file)) + unify._main( + argv=['my_fake_program', '/non_existent_file_92394492929'], + standard_out=None, + standard_error=output_file)) self.assertIn(non_existent_filename, output_file.getvalue()) def test_in_place(self): - with temporary_file('''\ -if True: - x = "abc" -''') as filename: + with temporary_file(dedent('''\ + if True: + x = "abc" + ''')) as filename: output_file = io.StringIO() self.assertEqual( unify._main(argv=['my_fake_program', '--in-place', filename], standard_out=output_file, standard_error=None), - None, - ) + None) + with open(filename) as f: - self.assertEqual('''\ -if True: - x = 'abc' -''', f.read()) + self.assertEqual( + dedent('''\ + if True: + x = 'abc' + '''), + f.read()) def test_in_place_precedence_over_check_only(self): - with temporary_file('''\ -if True: - x = "abc" -''') as filename: + with temporary_file(dedent('''\ + if True: + x = "abc" + ''')) as filename: output_file = io.StringIO() self.assertEqual( unify._main(argv=['my_fake_program', @@ -301,23 +305,27 @@ def test_in_place_precedence_over_check_only(self): filename], standard_out=output_file, standard_error=None), - None, - ) + None) + with open(filename) as f: - self.assertEqual('''\ -if True: - x = 'abc' -''', f.read()) + self.assertEqual( + dedent('''\ + if True: + x = 'abc' + '''), + f.read()) def test_ignore_hidden_directories(self): with temporary_directory() as directory: with temporary_directory(prefix='.', directory=directory) as inner_directory: - with temporary_file("""\ -if True: - x = "abc" -""", directory=inner_directory): + with temporary_file( + dedent("""\ + if True: + x = "abc" + """), + directory=inner_directory): output_file = io.StringIO() self.assertEqual( @@ -326,11 +334,9 @@ def test_ignore_hidden_directories(self): directory], standard_out=output_file, standard_error=None), - None, - ) - self.assertEqual( - '', - output_file.getvalue().strip()) + None) + + self.assertEqual('', output_file.getvalue().strip()) @contextlib.contextmanager diff --git a/unify.py b/unify.py index 24734d7..89e297f 100755 --- a/unify.py +++ b/unify.py @@ -175,6 +175,7 @@ def _drop_escape_bs(string): body = ''.join(splitted_body) return body + class SimpleEscapeFstring(SimpleEscapeString): """ F-string with one type of quote in body. @@ -219,8 +220,7 @@ def _format_code(source): editable_string.reformat() token_string = editable_string.token - modified_tokens.append( - (token_type, token_string, start, end, line)) + modified_tokens.append((token_type, token_string, start, end, line)) return untokenize.untokenize(modified_tokens) @@ -348,11 +348,14 @@ def _main(argv, standard_out, standard_error): name = filenames.pop(0) if args.recursive and os.path.isdir(name): for root, directories, children in os.walk(unicode(name)): - filenames += [os.path.join(root, f) for f in children - if f.endswith('.py') and - not f.startswith('.')] - directories[:] = [d for d in directories - if not d.startswith('.')] + filenames += [ + os.path.join(root, f) for f in children + if f.endswith('.py') and not f.startswith('.') + ] + + directories[:] = [ + d for d in directories if not d.startswith('.') + ] else: try: if format_file(name, args=args, standard_out=standard_out): From f2bc61e6faaf1b43973f42dd4af900c5dcedb175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Tue, 27 Oct 2020 10:38:59 +0500 Subject: [PATCH 09/17] nonglobal rules --- test_unify.py | 133 +++++++++++++++++++++++++++----------------------- unify.py | 36 +++++++------- 2 files changed, 89 insertions(+), 80 deletions(-) diff --git a/test_unify.py b/test_unify.py index 889df60..3890ca4 100755 --- a/test_unify.py +++ b/test_unify.py @@ -21,128 +21,138 @@ class TestUnitsSimpleString(unittest.TestCase): def test_preferred_single(self): - unify.rules['preferred_quote'] = "'" + rules = {'preferred_quote': "'"} - result = unify.format_code('"foo"') + result = unify.format_code('"foo"', rules) self.assertEqual(result, "'foo'") - result = unify.format_code('f"foo"') + result = unify.format_code('f"foo"', rules) self.assertEqual(result, "f'foo'") - result = unify.format_code('r"foo"') + result = unify.format_code('r"foo"', rules) self.assertEqual(result, "r'foo'") - result = unify.format_code('u"foo"') + result = unify.format_code('u"foo"', rules) self.assertEqual(result, "u'foo'") - result = unify.format_code('b"foo"') + result = unify.format_code('b"foo"', rules) self.assertEqual(result, "b'foo'") def test_preferred_double(self): - unify.rules['preferred_quote'] = '"' + rules = {'preferred_quote': '"'} - result = unify.format_code("'foo'") + result = unify.format_code("'foo'", rules) self.assertEqual(result, '"foo"') - result = unify.format_code("f'foo'") + result = unify.format_code("f'foo'", rules) self.assertEqual(result, 'f"foo"') - result = unify.format_code("r'foo'") + result = unify.format_code("r'foo'", rules) self.assertEqual(result, 'r"foo"') - result = unify.format_code("u'foo'") + result = unify.format_code("u'foo'", rules) self.assertEqual(result, 'u"foo"') - result = unify.format_code("b'foo'") + result = unify.format_code("b'foo'", rules) self.assertEqual(result, 'b"foo"') def test_keep_single(self): - unify.rules['preferred_quote'] = "'" - result = unify.format_code("'foo'") + rules = {'preferred_quote': "'"} + result = unify.format_code("'foo'", rules) self.assertEqual(result, "'foo'") def test_keep_double(self): - unify.rules['preferred_quote'] = '"' - result = unify.format_code('"foo"') + rules = {'preferred_quote': '"'} + result = unify.format_code('"foo"', rules) self.assertEqual(result, '"foo"') class TestUnitsSimpleQuotedString(unittest.TestCase): def test_opposite(self): - unify.rules['preferred_quote'] = "'" - unify.rules['escape_simple'] = 'opposite' + rules = { + 'preferred_quote': "'", + 'escape_simple': 'opposite', + } - result = unify.format_code('''"foo's"''') + result = unify.format_code('''"foo's"''', rules) self.assertEqual(result, '''"foo's"''') - result = unify.format_code("""'foo"s'""") + result = unify.format_code("""'foo"s'""", rules) self.assertEqual(result, """'foo"s'""") - result = unify.format_code('''"foo\\"s"''') + result = unify.format_code('''"foo\\"s"''', rules) self.assertEqual(result, """'foo"s'""") - result = unify.format_code("""'foo\\'s'""") + result = unify.format_code("""'foo\\'s'""", rules) self.assertEqual(result, '''"foo's"''') def test_backslash(self): - unify.rules['preferred_quote'] = "'" - unify.rules['escape_simple'] = 'backslash' + rules = { + 'preferred_quote': "'", + 'escape_simple': 'backslash', + } - result = unify.format_code('''"foo's"''') + result = unify.format_code('''"foo's"''', rules) self.assertEqual(result, """'foo\\'s'""") - result = unify.format_code("""'foo"s'""") + result = unify.format_code("""'foo"s'""", rules) self.assertEqual(result, """'foo"s'""") - result = unify.format_code('''"foo\\"s"''') + result = unify.format_code('''"foo\\"s"''', rules) self.assertEqual(result, """'foo"s'""") - result = unify.format_code("""'foo\\'s'""") + result = unify.format_code("""'foo\\'s'""", rules) self.assertEqual(result, """'foo\\'s'""") def test_keep_unformatted(self): - unify.rules['preferred_quote'] = "'" - unify.rules['escape_simple'] = 'opposite' + rules = { + 'preferred_quote': "'", + 'escape_simple': 'opposite', + } - result = unify.format_code('''f"foo's{some_var}"''') + result = unify.format_code('''f"foo's{some_var}"''', rules) self.assertEqual(result, '''f"foo's{some_var}"''') - result = unify.format_code("""r'foo\\'s'""") + result = unify.format_code("""r'foo\\'s'""", rules) self.assertEqual(result, """r'foo\\'s'""") def test_backslash_train(self): - unify.rules['preferred_quote'] = "'" - unify.rules['escape_simple'] = 'opposite' + rules = { + 'preferred_quote': "'", + 'escape_simple': 'opposite', + } + + result = unify.format_code('''"a'b\\'c\\\\'d\\\\\\'e\\\\\\\\'f"''', + rules) - result = unify.format_code('''"a'b\\'c\\\\'d\\\\\\'e\\\\\\\\'f"''') self.assertEqual(result, '''"a'b'c\\\\'d\\\\'e\\\\\\\\'f"''') - result = unify.format_code('''"\\'a"''') + result = unify.format_code('''"\\'a"''', rules) self.assertEqual(result, '''"'a"''') - result = unify.format_code('''"\\\\'a"''') + result = unify.format_code('''"\\\\'a"''', rules) self.assertEqual(result, '''"\\\\'a"''') class TestUnitsTripleQuote(unittest.TestCase): def test_no_change(self): - unify.rules['preferred_quote'] = "'" + rules = {'preferred_quote': "'"} - result = unify.format_code('''"""foo"""''') + result = unify.format_code('''"""foo"""''', rules) self.assertEqual(result, '''"""foo"""''') - result = unify.format_code('''f"""foo"""''') + result = unify.format_code('''f"""foo"""''', rules) self.assertEqual(result, '''f"""foo"""''') - result = unify.format_code('''r"""\\t"""''') + result = unify.format_code('''r"""\\t"""''', rules) self.assertEqual(result, '''r"""\\t"""''') - result = unify.format_code('''u"""foo"""''') + result = unify.format_code('''u"""foo"""''', rules) self.assertEqual(result, '''u"""foo"""''') - result = unify.format_code('''b"""foo"""''') + result = unify.format_code('''b"""foo"""''', rules) self.assertEqual(result, '''b"""foo"""''') @@ -153,58 +163,59 @@ def test_detect_encoding_with_bad_encoding(self): self.assertEqual('latin-1', unify.detect_encoding(filename)) def test_format_code(self): - unify.rules['preferred_quote'] = "'" + rules = {'preferred_quote': "'"} self.assertEqual("x = 'abc' \\\n'next'\n", - unify.format_code('x = "abc" \\\n"next"\n')) + unify.format_code('x = "abc" \\\n"next"\n', rules)) self.assertEqual("x = f'abc' \\\nf'next'\n", - unify.format_code('x = f"abc" \\\nf"next"\n')) + unify.format_code('x = f"abc" \\\nf"next"\n', rules)) self.assertEqual("x = u'abc' \\\nu'next'\n", - unify.format_code('x = u"abc" \\\nu"next"\n')) + unify.format_code('x = u"abc" \\\nu"next"\n', rules)) self.assertEqual("x = b'abc' \\\nb'next'\n", - unify.format_code('x = b"abc" \\\nb"next"\n')) + unify.format_code('x = b"abc" \\\nb"next"\n', rules)) def test_format_code_with_backslash_in_comment(self): - unify.rules['preferred_quote'] = "'" + rules = {'preferred_quote': "'"} self.assertEqual("x = 'abc' #\\\n'next'\n", - unify.format_code('x = "abc" #\\\n"next"\n')) + unify.format_code('x = "abc" #\\\n"next"\n', rules)) self.assertEqual("x = f'abc' #\\\nf'next'\n", - unify.format_code('x = f"abc" #\\\nf"next"\n')) + unify.format_code('x = f"abc" #\\\nf"next"\n', rules)) self.assertEqual("x = r'abc' #\\\nr'next'\n", - unify.format_code('x = r"abc" #\\\nr"next"\n')) + unify.format_code('x = r"abc" #\\\nr"next"\n', rules)) self.assertEqual("x = r'abc' \\\nr'next'\n", - unify.format_code('x = r"abc" \\\nr"next"\n')) + unify.format_code('x = r"abc" \\\nr"next"\n', rules)) self.assertEqual("x = u'abc' #\\\nu'next'\n", - unify.format_code('x = u"abc" #\\\nu"next"\n')) + unify.format_code('x = u"abc" #\\\nu"next"\n', rules)) self.assertEqual("x = b'abc' #\\\nb'next'\n", - unify.format_code('x = b"abc" #\\\nb"next"\n')) + unify.format_code('x = b"abc" #\\\nb"next"\n', rules)) def test_format_code_with_syntax_error(self): - unify.rules['preferred_quote'] = "'" + rules = {'preferred_quote': "'"} self.assertEqual('foo("abc"\n', - unify.format_code('foo("abc"\n')) + unify.format_code('foo("abc"\n', rules)) self.assertEqual('foo(f"abc"\n', - unify.format_code('foo(f"abc"\n')) + unify.format_code('foo(f"abc"\n', rules)) self.assertEqual('foo(r"Tabs \t, new lines \n."\n', - unify.format_code('foo(r"Tabs \t, new lines \n."\n')) + unify.format_code('foo(r"Tabs \t, new lines \n."\n', + rules)) self.assertEqual('foo(u"abc"\n', - unify.format_code('foo(u"abc"\n')) + unify.format_code('foo(u"abc"\n', rules)) self.assertEqual('foo(b"abc"\n', - unify.format_code('foo(b"abc"\n')) + unify.format_code('foo(b"abc"\n', rules)) class TestSystem(unittest.TestCase): diff --git a/unify.py b/unify.py index 89e297f..ae66b09 100755 --- a/unify.py +++ b/unify.py @@ -47,16 +47,12 @@ unicode = str -# dict with transform rules -rules = {} - - class AbstractString: """Interface to transform strings.""" __metaclass__ = ABCMeta @abstractmethod - def reformat(self): pass + def reformat(self, rules): pass @abstractproperty def token(self): pass @@ -75,7 +71,7 @@ class ImmutableString(AbstractString): def __init__(self, body): self.body = body - def reformat(self): pass + def reformat(self, rules): pass @property def token(self): @@ -100,7 +96,7 @@ def __init__(self, prefix, quote, body): self.old_prefix = prefix self.old_quote = quote - def reformat(self): + def reformat(self, rules): preferred_quote = rules['preferred_quote'] self.quote = preferred_quote @@ -133,7 +129,7 @@ def __init__(self, prefix, quote, body): self.old_quote = quote self.old_body = body - def reformat(self): + def reformat(self, rules): preferred_quote = rules['preferred_quote'] escape_simple = rules['escape_simple'] quote_in_body = "'" if "'" in self.body else '"' @@ -184,25 +180,25 @@ class SimpleEscapeFstring(SimpleEscapeString): Use escape_simple and preferred_quote rules. """ - def reformat(self): + def reformat(self, rules): if any(br in self.body for br in '{}'): # don't transform since can't use backslashes in bracket area # TODO add body parsing and handle this case return # can treat this case as simple escape - super().reformat() + super().reformat(rules) -def format_code(source): +def format_code(source, rules): """Return source code with quotes unified.""" try: - return _format_code(source) + return _format_code(source, rules) except (tokenize.TokenError, IndentationError): return source -def _format_code(source): +def _format_code(source, rules): """Return source code with quotes unified.""" if not source: return source @@ -217,7 +213,7 @@ def _format_code(source): line) in tokenize.generate_tokens(sio.readline): editable_string = get_editable_string(token_type, token_string) - editable_string.reformat() + editable_string.reformat(rules) token_string = editable_string.token modified_tokens.append((token_type, token_string, start, end, line)) @@ -278,7 +274,7 @@ def detect_encoding(filename): return 'latin-1' -def format_file(filename, args, standard_out): +def format_file(filename, args, standard_out, rules): """Run format_code() on a file. Returns `True` if any changes are needed and they are not being done @@ -288,7 +284,7 @@ def format_file(filename, args, standard_out): encoding = detect_encoding(filename) with open_with_encoding(filename, encoding=encoding) as input_file: source = input_file.read() - formatted_source = format_code(source) + formatted_source = format_code(source, rules) if source != formatted_source: if args.in_place: @@ -339,8 +335,10 @@ def _main(argv, standard_out, standard_error): args = parser.parse_args(argv[1:]) - rules['preferred_quote'] = args.quote - rules['escape_simple'] = args.escape_simple + rules = { + 'preferred_quote': args.quote, + 'escape_simple': args.escape_simple, + } filenames = list(set(args.files)) changes_needed = False failure = False @@ -358,7 +356,7 @@ def _main(argv, standard_out, standard_error): ] else: try: - if format_file(name, args=args, standard_out=standard_out): + if format_file(name, args=args, standard_out=standard_out, rules=rules): changes_needed = True except IOError as exception: print(unicode(exception), file=standard_error) From d4266c1c95dcbe2bb5fa806ebb8be353dd62e809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Sat, 31 Oct 2020 02:58:09 +0500 Subject: [PATCH 10/17] add f-string parser. --- test_unify.py | 56 ++++++++++++++++++ unify.py | 155 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 203 insertions(+), 8 deletions(-) diff --git a/test_unify.py b/test_unify.py index 3890ca4..16be811 100755 --- a/test_unify.py +++ b/test_unify.py @@ -156,6 +156,62 @@ def test_no_change(self): self.assertEqual(result, '''b"""foo"""''') +class TestFstringParser(unittest.TestCase): + + def test_find_expression_areas(self): + cases = [ + ('text', []), + ('{bcd}', [(0, 5)]), + ('{{not exp area}}', []), + ('{{{def}}}', [(2, 7)]), + ('{b}{e}', [(0, 3),(3, 6)]), + ('{bcd}}}', [(0, 5)]), + ('{{{def}', [(2, 7)]), + ] + for source, expected in cases: + with self.subTest(body=source): + parser = unify.FstringParser(source) + result = parser.find_expression_areas() + self.assertEqual(result, expected) + + def test_parse(self): + cases = [ + ('text', ['text']), + ('{bcd}', ['{bcd}']), + ('{{not exp area}}', ['{{not exp area}}']), + ('{{{def}}}', ['{{', '{def}', '}}']), + ('{b}{e}', ['{b}', '{e}']), + ('{bcd}}}', ['{bcd}', '}}']), + ('{{{def}', ['{{', '{def}']), + ('{b}d{f}', ['{b}', 'd', '{f}']), + ] + for source, expected in cases: + with self.subTest(body=source): + parser = unify.FstringParser(source) + parser.parse() + result = parser.parsed_body + self.assertEqual(result, expected) + + def test_indexfy(self): + cases = [ + ('text', ['text'], [], []), + ('{bcd}', [], ['{bcd}'], [0]), + ('{{not exp area}}', ['{{not exp area}}'], [], []), + ('{{{def}}}', ['{{', '}}'], ['{def}'],[1]), + ('{b}{e}', [], ['{b}', '{e}'], [0, 1]), + ('{bcd}}}', ['}}'], ['{bcd}'], [0]), + ('{{{def}', ['{{'], ['{def}'], [1]), + ('{b}d{f}', ['d'], ['{b}', '{f}'], [0, 2]), + ] + for source, expected_texts, expected_expr, expected_ids in cases: + with self.subTest(body=source): + parser = unify.FstringParser(source) + parser.parse() + self.assertEqual(parser.texts, expected_texts) + self.assertEqual(parser.expressions, expected_expr) + self.assertEqual(parser.expression_ids, expected_ids) + + class TestUnitsCode(unittest.TestCase): def test_detect_encoding_with_bad_encoding(self): diff --git a/unify.py b/unify.py index ae66b09..4f19077 100755 --- a/unify.py +++ b/unify.py @@ -29,6 +29,7 @@ from abc import ABCMeta, abstractmethod, abstractproperty import io +from itertools import chain, count import os import re import signal @@ -172,7 +173,7 @@ def _drop_escape_bs(string): return body -class SimpleEscapeFstring(SimpleEscapeString): +class SimpleEscapeFstring(AbstractString): """ F-string with one type of quote in body. @@ -180,14 +181,134 @@ class SimpleEscapeFstring(SimpleEscapeString): Use escape_simple and preferred_quote rules. """ + def __init__(self, prefix, quote, body, parsed_body, expr_ids): + self.prefix = prefix + self.quote = quote + self.body = body + self.parsed_body = parsed_body + self.expr_ids = expr_ids + self.old_prefix = prefix + self.old_quote = quote + self.old_body = body + def reformat(self, rules): - if any(br in self.body for br in '{}'): - # don't transform since can't use backslashes in bracket area - # TODO add body parsing and handle this case + # Not implemented yet + pass + + @property + def token(self): + return '{prefix}{quote}{body}{quote}'.format( + prefix=self.prefix, quote=self.quote, body=self.body + ) + + @property + def old_token(self): + return '{prefix}{quote}{body}{quote}'.format( + prefix=self.old_prefix, quote=self.old_quote, body=self.old_body + ) + + +class FstringParser: + """Parse f-string to text and expression parts.""" + + def __init__(self, body): + self.body = body + self.parsed_body = None + self.expr_area_idx = None + self.texts = None + self.expressions = None + self.expression_ids = None + + def parse(self): + expr_areas = self.find_expression_areas() + self._parse(expr_areas) + self._indexfy_parsed_body() + + def _parse(self, expr_areas): + if not expr_areas: + self.parsed_body = [self.body] return - # can treat this case as simple escape - super().reformat(rules) + parsed_body = [] + if expr_areas[0][0] != 0: + parsed_body.append(self.body[:expr_areas[0][0]]) + + next_areas = chain(expr_areas, [(len(self.body), None)]) + next(next_areas) + for (cur_start, cur_end), (next_start, _) in zip(expr_areas, next_areas): + chunk = self.body[cur_start:cur_end] + parsed_body.append(chunk) + if cur_end != next_start: + chunk = self.body[cur_end:next_start] + parsed_body.append(chunk) + self.parsed_body = parsed_body + + def find_expression_areas(self): + """ + Work like state machine. + + Has two states: outside expression area and inside it. + If outside it looks for opening brace, ensure that this is not escape brace and + switch to inside mode. + If inside it counts braces to search close pair. If found switch to outside mode. + Return list of numbers for slice. + """ + expression_area = [] + expr_area_mode = False + escape_mark = False + brace_count = 0 + next_chars = chain(self.body, [None]) + next(next_chars) + start_expr_area = None + for pos, cur_char, next_char in zip(count(), self.body, next_chars): + if not expr_area_mode: + if escape_mark: + escape_mark = False + continue + if cur_char == '{' and next_char == '{': + escape_mark = True + continue + if cur_char == '{': + expr_area_mode = True + start_expr_area = pos + brace_count += 1 + else: + if cur_char == '{': + brace_count += 1 + if cur_char == '}': + brace_count -= 1 + if cur_char == '}' and brace_count == 0: + end_expr_area = pos + 1 + expression_area.append((start_expr_area,end_expr_area)) + expr_area_mode = False + return expression_area + + def _indexfy_parsed_body(self): + re_expr = re.compile(r'{[^{].*[^}]}|{.}') + texts = [] + expressions = [] + expression_ids = [] + for i, chunk in enumerate(self.parsed_body): + if re_expr.match(chunk): + expressions.append(chunk) + expression_ids.append(i) + else: + texts.append(chunk) + self.texts = texts + self.expressions = expressions + self.expression_ids = expression_ids + + def text_has_single_quote(self): + return any("'" in tx for tx in self.texts) + + def text_has_double_quote(self): + return any('"' in tx for tx in self.texts) + + def expression_has_single_quote(self): + return any("'" in expr for expr in self.expressions) + + def expression_has_double_quote(self): + return any('"' in expr for expr in self.expressions) def format_code(source, rules): @@ -242,12 +363,30 @@ def get_editable_string(token_type, token_string): # don't transform raw string since can't use backslash # as escape char return ImmutableString(token_string) + if 'f' in parsed_string['prefix'].lower(): + parser = FstringParser(parsed_string['body']) + parser.parse() + text_has_single = parser.text_has_single_quote() + text_has_double = parser.text_has_double_quote() + expression_has_single = parser.expression_has_single_quote() + expression_has_double = parser.expression_has_double_quote() + + if text_has_single and text_has_double: + # don't transform complicated escape yet + return ImmutableString(token_string) + if text_has_single or text_has_double: + if not expression_has_single and expression_has_double: + # treat this case as simple string + return SimpleEscapeString(**parsed_string) + return SimpleEscapeFstring( + **parsed_string, + parsed_body=parser.parsed_body, + expr_ids=parser.expression_ids + ) if all(qt in parsed_string['body'] for qt in ("'", '"')): # don't transform complicated escape yet return ImmutableString(token_string) if any(qt in parsed_string['body'] for qt in ("'", '"')): - if 'f' in parsed_string['prefix'].lower(): - return SimpleEscapeFstring(**parsed_string) return SimpleEscapeString(**parsed_string) return ImmutableString(token_string) From 5708d0a1769d8699473db8c32c50817211bcec58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Mon, 2 Nov 2020 10:00:16 +0500 Subject: [PATCH 11/17] examples added. --- README.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 07e4d10..63e0441 100644 --- a/README.rst +++ b/README.rst @@ -20,12 +20,16 @@ this code .. code-block:: python - x = "abc" - y = 'hello' + a = "abc" + b = 'hello' + c = 'this \' is quote' + d = 'this " is another quote' gets formatted into this .. code-block:: python - x = 'abc' - y = 'hello' + a = 'abc' + b = 'hello' + c= "this ' is quote" + d = 'this " is another quote' From 4bed509fa9bd5553802c89cafad33fd025db7401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Mon, 2 Nov 2020 10:09:25 +0500 Subject: [PATCH 12/17] add testcases. --- test_unify.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test_unify.py b/test_unify.py index 16be811..a75643f 100755 --- a/test_unify.py +++ b/test_unify.py @@ -184,6 +184,9 @@ def test_parse(self): ('{bcd}}}', ['{bcd}', '}}']), ('{{{def}', ['{{', '{def}']), ('{b}d{f}', ['{b}', 'd', '{f}']), + ('{ {1} }', ['{ {1} }']), + ('{{1}}', ['{{1}}']), + ('{ {{1,2}.pop()} }', ['{ {{1,2}.pop()} }']), ] for source, expected in cases: with self.subTest(body=source): @@ -202,6 +205,9 @@ def test_indexfy(self): ('{bcd}}}', ['}}'], ['{bcd}'], [0]), ('{{{def}', ['{{'], ['{def}'], [1]), ('{b}d{f}', ['d'], ['{b}', '{f}'], [0, 2]), + ('{ {1} }', [], ['{ {1} }'],[0]), + ('{{1}}', ['{{1}}'], [],[]), + ('{ {{1,2}.pop()} }', [], ['{ {{1,2}.pop()} }'], [0]), ] for source, expected_texts, expected_expr, expected_ids in cases: with self.subTest(body=source): From 99ccc5dd9f872886a55a451e6479822a8093ef03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Mon, 2 Nov 2020 21:59:19 +0500 Subject: [PATCH 13/17] f-string handle added. --- test_unify.py | 103 +++++++++++++++++++++++++++++++++++++++++++++++-- unify.py | 104 ++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 196 insertions(+), 11 deletions(-) diff --git a/test_unify.py b/test_unify.py index a75643f..5efe26c 100755 --- a/test_unify.py +++ b/test_unify.py @@ -110,10 +110,6 @@ def test_keep_unformatted(self): 'preferred_quote': "'", 'escape_simple': 'opposite', } - - result = unify.format_code('''f"foo's{some_var}"''', rules) - self.assertEqual(result, '''f"foo's{some_var}"''') - result = unify.format_code("""r'foo\\'s'""", rules) self.assertEqual(result, """r'foo\\'s'""") @@ -135,6 +131,105 @@ def test_backslash_train(self): self.assertEqual(result, '''"\\\\'a"''') +class TestUnitsSimpleQuotedFstring(unittest.TestCase): + + def test_no_quote_in_expression_area(self): + # don't add 'f_string_expression_quote' to ensure it's + # handled by SimpleQuotedString + rules = { + 'preferred_quote': "'", + 'escape_simple': 'opposite', + } + result = unify.format_code('''f"foo's{some_var}"''', rules) + self.assertEqual(result, '''f"foo's{some_var}"''') + + rules = { + 'preferred_quote': "'", + 'escape_simple': 'backslash', + } + result = unify.format_code('''f"foo's{some_var}"''', rules) + self.assertEqual(result, """f'foo\\'s{some_var}'""") + + rules = { + 'preferred_quote': '"', + 'escape_simple': 'opposite', + } + result = unify.format_code('''f"foo's{some_var}"''', rules) + self.assertEqual(result, '''f"foo's{some_var}"''') + + def test_single_quote_in_expr_area(self): + rules = { + 'preferred_quote': "'", + 'escape_simple': 'opposite', + 'f_string_expression_quote': 'single', + } + result = unify.format_code('''f"foo{some_dict['a']}"''', rules) + self.assertEqual(result, '''f"foo{some_dict['a']}"''') + + result = unify.format_code('''f"foo's{some_dict['a']}"''', rules) + self.assertEqual(result, '''f"foo's{some_dict['a']}"''') + + result = unify.format_code("""f'foo "name" {some_dict["a"]}'""", rules) + self.assertEqual(result, '''f"foo \\"name\\" {some_dict['a']}"''') + + def test_double_quote_in_expr_area(self): + rules = { + 'preferred_quote': "'", + 'escape_simple': 'opposite', + 'f_string_expression_quote': 'double', + } + result = unify.format_code("""f'foo{some_dict["a"]}'""", rules) + self.assertEqual(result, """f'foo{some_dict["a"]}'""") + + result = unify.format_code("""f'foo "name" {some_dict["a"]}'""", rules) + self.assertEqual(result, """f'foo "name" {some_dict["a"]}'""") + + result = unify.format_code('''f"foo's{some_dict['a']}"''', rules) + self.assertEqual(result, """f'foo\\'s{some_dict["a"]}'""") + + def test_depended_opposite(self): + rules = { + 'preferred_quote': "'", + 'escape_simple': 'opposite', + 'f_string_expression_quote': 'depended', + } + result = unify.format_code('''f"foo{some_dict['a']}"''', rules) + self.assertEqual(result, '''f"foo{some_dict['a']}"''') + + result = unify.format_code("""f'foo{some_dict["a"]}'""", rules) + self.assertEqual(result, '''f"foo{some_dict['a']}"''') + + result = unify.format_code('''f"foo's{some_dict['a']}"''', rules) + self.assertEqual(result, '''f"foo's{some_dict['a']}"''') + + result = unify.format_code('''f"foo \\'name\\' {some_dict['a']}"''', rules) + self.assertEqual(result, '''f"foo 'name' {some_dict['a']}"''') + + result = unify.format_code("""f'foo "name" {some_dict["a"]}'""", rules) + self.assertEqual(result, """f'foo "name" {some_dict["a"]}'""") + + def test_depended_backskash(self): + rules = { + 'preferred_quote': "'", + 'escape_simple': 'backslash', + 'f_string_expression_quote': 'depended', + } + result = unify.format_code('''f"foo{some_dict['a']}"''', rules) + self.assertEqual(result, """f'foo{some_dict["a"]}'""") + + result = unify.format_code("""f'foo{some_dict["a"]}'""", rules) + self.assertEqual(result, """f'foo{some_dict["a"]}'""") + + result = unify.format_code('''f"foo's{some_dict['a']}"''', rules) + self.assertEqual(result, """f'foo\\'s{some_dict["a"]}'""") + + result = unify.format_code('''f"foo 'name' {some_dict['a']}"''', rules) + self.assertEqual(result, """f'foo \\'name\\' {some_dict["a"]}'""") + + result = unify.format_code("""f'foo "name" {some_dict["a"]}'""", rules) + self.assertEqual(result, """f'foo "name" {some_dict["a"]}'""") + + class TestUnitsTripleQuote(unittest.TestCase): def test_no_change(self): diff --git a/unify.py b/unify.py index 4f19077..3f48ef0 100755 --- a/unify.py +++ b/unify.py @@ -87,7 +87,7 @@ class SimpleString(AbstractString): """ String without quote in body. - Use prefered_quote rule. + Use preferred_quote rule. """ def __init__(self, prefix, quote, body): @@ -180,6 +180,7 @@ class SimpleEscapeFstring(AbstractString): Not fully implemented. Use escape_simple and preferred_quote rules. """ + OPPOSITE_QUOTE = {"'": '"', '"': "'"} def __init__(self, prefix, quote, body, parsed_body, expr_ids): self.prefix = prefix @@ -187,13 +188,97 @@ def __init__(self, prefix, quote, body, parsed_body, expr_ids): self.body = body self.parsed_body = parsed_body self.expr_ids = expr_ids + self.text_ids = sorted(set(range(len(parsed_body))) - set(expr_ids)) + self.expressions = [parsed_body[i] for i in expr_ids] + self.texts = [parsed_body[i] for i in self.text_ids] self.old_prefix = prefix self.old_quote = quote self.old_body = body def reformat(self, rules): - # Not implemented yet - pass + if rules['f_string_expression_quote'] == 'ignore': return + + outer_quote, expr_quote = self._get_quotes(rules) + + self.quote = outer_quote + self._update_texts(outer_quote) + self._update_expressions(expr_quote) + self.body = ''.join(self.parsed_body) + + def _get_quotes(self, rules): + """ + get quotes depending of rules and quotes in body. + + If f_string_expression_quote is 'single' or 'double' then outer quote is + opposites. + If f_string_expression_quote is 'depended' it use table choices. + Keys to choose: + (preferred_quote, escape_simple, text_quote) + + Return values: + (outer_quote, expr_quote) + """ + escape_simple = rules['escape_simple'] + f_string_expr_quote = rules['f_string_expression_quote'] + if escape_simple == 'ignore': + return self.quote, self.OPPOSITE_QUOTE[self.quote] + if f_string_expr_quote == 'single': + return '"', "'" + if f_string_expr_quote == 'double': + return "'", '"' + + if f_string_expr_quote == 'depended': + outer_quote_choices = { + ("'", 'opp', None): '"', + ("'", 'opp', "'"): '"', + ("'", 'opp', '"'): "'", + ("'", 'bs', None): "'", + ("'", 'bs', "'"): "'", + ("'", 'bs', '"'): "'", + ('"', 'opp', None): "'", + ('"', 'opp', "'"): '"', + ('"', 'opp', '"'): "'", + ('"', 'bs', None): '"', + ('"', 'bs', "'"): '"', + ('"', 'bs', '"'): '"', + } + preferred_qoute = rules['preferred_quote'] + escape_simple = 'opp' if escape_simple == 'opposite' else 'bs' + text_qoute = self._find_text_quote() + key = (preferred_qoute, escape_simple, text_qoute) + + outer_quote = outer_quote_choices[key] + expr_quote = self.OPPOSITE_QUOTE[outer_quote] + return outer_quote, expr_quote + return self.quote, self.OPPOSITE_QUOTE[self.quote] + + def _update_texts(self, outer_quote): + quote_in_body = self._find_text_quote() + if quote_in_body is None: return + + replace_quote = quote_in_body + if quote_in_body == outer_quote: + replace_quote = '\\' + replace_quote + + for i in self.text_ids: + txt = self.parsed_body[i] + txt = drop_escape_backslash(txt) + txt = txt.replace(quote_in_body, replace_quote) + self.parsed_body[i] = txt + + def _update_expressions(self, expr_quote): + for i in self.expr_ids: + expr = self.parsed_body[i] + expr = expr.replace("'", expr_quote).replace('"', expr_quote) + self.parsed_body[i] = expr + + def _find_text_quote(self): + quote = None + if any("'" in txt for txt in self.texts): + quote = "'" + if any('"' in txt for txt in self.texts): + quote = '"' + return quote @property def token(self): @@ -368,14 +453,14 @@ def get_editable_string(token_type, token_string): parser.parse() text_has_single = parser.text_has_single_quote() text_has_double = parser.text_has_double_quote() - expression_has_single = parser.expression_has_single_quote() - expression_has_double = parser.expression_has_double_quote() if text_has_single and text_has_double: # don't transform complicated escape yet return ImmutableString(token_string) - if text_has_single or text_has_double: - if not expression_has_single and expression_has_double: + else: + expression_has_single = parser.expression_has_single_quote() + expression_has_double = parser.expression_has_double_quote() + if not expression_has_single and not expression_has_double: # treat this case as simple string return SimpleEscapeString(**parsed_string) return SimpleEscapeFstring( @@ -467,6 +552,10 @@ def _main(argv, standard_out, standard_error): help='escape strategy if string has one type of quote', choices=['opposite', 'backslash', 'ignore'], default='opposite') + parser.add_argument('--f-string-expression-quote', + help='quote inside expressions in f-string.', + choices=['ignore', 'depended', 'single', 'double'], + default='ignore') parser.add_argument('--version', action='version', version='%(prog)s ' + __version__) parser.add_argument('files', nargs='+', @@ -477,6 +566,7 @@ def _main(argv, standard_out, standard_error): rules = { 'preferred_quote': args.quote, 'escape_simple': args.escape_simple, + 'f_string_expression_quote': args.f_string_expression_quote, } filenames = list(set(args.files)) changes_needed = False From 0d1c817813366a1461003e720bb6d2ea7ad20b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Mon, 2 Nov 2020 22:54:16 +0500 Subject: [PATCH 14/17] raw f-string case added. --- test_unify.py | 23 +++++++++++++++++++++++ unify.py | 14 +++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/test_unify.py b/test_unify.py index 5efe26c..338ff4b 100755 --- a/test_unify.py +++ b/test_unify.py @@ -229,6 +229,29 @@ def test_depended_backskash(self): result = unify.format_code("""f'foo "name" {some_dict["a"]}'""", rules) self.assertEqual(result, """f'foo "name" {some_dict["a"]}'""") + def test_raw_string(self): + rules = { + 'preferred_quote': "'", + 'escape_simple': 'opposite', + 'f_string_expression_quote': 'depended', + } + result = unify.format_code('''rf"foo{some_dict['a']}"''', rules) + self.assertEqual(result, '''rf"foo{some_dict['a']}"''') + + result = unify.format_code("""rf'foo{some_dict["a"]}'""", rules) + self.assertEqual(result, '''rf"foo{some_dict['a']}"''') + + rules = { + 'preferred_quote': "'", + 'escape_simple': 'backslash', + 'f_string_expression_quote': 'depended', + } + result = unify.format_code('''rf"foo{some_dict['a']}"''', rules) + self.assertEqual(result, """rf'foo{some_dict["a"]}'""") + + result = unify.format_code("""rf'foo{some_dict["a"]}'""", rules) + self.assertEqual(result, """rf'foo{some_dict["a"]}'""") + class TestUnitsTripleQuote(unittest.TestCase): diff --git a/unify.py b/unify.py index 3f48ef0..1317a55 100755 --- a/unify.py +++ b/unify.py @@ -446,7 +446,19 @@ def get_editable_string(token_type, token_string): return SimpleString(**parsed_string) if 'r' in parsed_string['prefix'].lower(): # don't transform raw string since can't use backslash - # as escape char + # as escape char. + # One exception is f-string with quote in expression area + if 'f' in parsed_string['prefix'].lower(): + parser = FstringParser(parsed_string['body']) + parser.parse() + text_has_single = parser.text_has_single_quote() + text_has_double = parser.text_has_double_quote() + if not text_has_single and not text_has_double: + return SimpleEscapeFstring( + **parsed_string, + parsed_body=parser.parsed_body, + expr_ids=parser.expression_ids + ) return ImmutableString(token_string) if 'f' in parsed_string['prefix'].lower(): parser = FstringParser(parsed_string['body']) From c7e8c2c91b02a75ab70866dd2dca972ffa94d3e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Mon, 2 Nov 2020 22:58:26 +0500 Subject: [PATCH 15/17] cleanup --- test_unify.py | 10 +++++----- unify.py | 36 ++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/test_unify.py b/test_unify.py index 338ff4b..9dab5b1 100755 --- a/test_unify.py +++ b/test_unify.py @@ -131,7 +131,7 @@ def test_backslash_train(self): self.assertEqual(result, '''"\\\\'a"''') -class TestUnitsSimpleQuotedFstring(unittest.TestCase): +class TestUnitsSimpleQuotedFormatString(unittest.TestCase): def test_no_quote_in_expression_area(self): # don't add 'f_string_expression_quote' to ensure it's @@ -282,7 +282,7 @@ def test_find_expression_areas(self): ('{bcd}', [(0, 5)]), ('{{not exp area}}', []), ('{{{def}}}', [(2, 7)]), - ('{b}{e}', [(0, 3),(3, 6)]), + ('{b}{e}', [(0, 3), (3, 6)]), ('{bcd}}}', [(0, 5)]), ('{{{def}', [(2, 7)]), ] @@ -318,13 +318,13 @@ def test_indexfy(self): ('text', ['text'], [], []), ('{bcd}', [], ['{bcd}'], [0]), ('{{not exp area}}', ['{{not exp area}}'], [], []), - ('{{{def}}}', ['{{', '}}'], ['{def}'],[1]), + ('{{{def}}}', ['{{', '}}'], ['{def}'], [1]), ('{b}{e}', [], ['{b}', '{e}'], [0, 1]), ('{bcd}}}', ['}}'], ['{bcd}'], [0]), ('{{{def}', ['{{'], ['{def}'], [1]), ('{b}d{f}', ['d'], ['{b}', '{f}'], [0, 2]), - ('{ {1} }', [], ['{ {1} }'],[0]), - ('{{1}}', ['{{1}}'], [],[]), + ('{ {1} }', [], ['{ {1} }'], [0]), + ('{{1}}', ['{{1}}'], [], []), ('{ {{1,2}.pop()} }', [], ['{ {{1,2}.pop()} }'], [0]), ] for source, expected_texts, expected_expr, expected_ids in cases: diff --git a/unify.py b/unify.py index 1317a55..15bed59 100755 --- a/unify.py +++ b/unify.py @@ -173,7 +173,7 @@ def _drop_escape_bs(string): return body -class SimpleEscapeFstring(AbstractString): +class SimpleEscapeFormatString(AbstractString): """ F-string with one type of quote in body. @@ -197,6 +197,7 @@ def __init__(self, prefix, quote, body, parsed_body, expr_ids): def reformat(self, rules): if rules['f_string_expression_quote'] == 'ignore': return + if rules['escape_simple'] == 'ignore': return outer_quote, expr_quote = self._get_quotes(rules) @@ -218,10 +219,7 @@ def _get_quotes(self, rules): Return values: (outer_quote, expr_quote) """ - escape_simple = rules['escape_simple'] f_string_expr_quote = rules['f_string_expression_quote'] - if escape_simple == 'ignore': - return self.quote, self.OPPOSITE_QUOTE[self.quote] if f_string_expr_quote == 'single': return '"', "'" if f_string_expr_quote == 'double': @@ -243,6 +241,7 @@ def _get_quotes(self, rules): ('"', 'bs', '"'): '"', } preferred_qoute = rules['preferred_quote'] + escape_simple = rules['escape_simple'] escape_simple = 'opp' if escape_simple == 'opposite' else 'bs' text_qoute = self._find_text_quote() key = (preferred_qoute, escape_simple, text_qoute) @@ -299,7 +298,6 @@ class FstringParser: def __init__(self, body): self.body = body self.parsed_body = None - self.expr_area_idx = None self.texts = None self.expressions = None self.expression_ids = None @@ -364,7 +362,7 @@ def find_expression_areas(self): brace_count -= 1 if cur_char == '}' and brace_count == 0: end_expr_area = pos + 1 - expression_area.append((start_expr_area,end_expr_area)) + expression_area.append((start_expr_area, end_expr_area)) expr_area_mode = False return expression_area @@ -454,7 +452,7 @@ def get_editable_string(token_type, token_string): text_has_single = parser.text_has_single_quote() text_has_double = parser.text_has_double_quote() if not text_has_single and not text_has_double: - return SimpleEscapeFstring( + return SimpleEscapeFormatString( **parsed_string, parsed_body=parser.parsed_body, expr_ids=parser.expression_ids @@ -469,17 +467,15 @@ def get_editable_string(token_type, token_string): if text_has_single and text_has_double: # don't transform complicated escape yet return ImmutableString(token_string) - else: - expression_has_single = parser.expression_has_single_quote() - expression_has_double = parser.expression_has_double_quote() - if not expression_has_single and not expression_has_double: - # treat this case as simple string - return SimpleEscapeString(**parsed_string) - return SimpleEscapeFstring( - **parsed_string, - parsed_body=parser.parsed_body, - expr_ids=parser.expression_ids - ) + expression_has_single = parser.expression_has_single_quote() + expression_has_double = parser.expression_has_double_quote() + if not expression_has_single and not expression_has_double: + # treat this case as simple string + return SimpleEscapeString(**parsed_string) + params = parsed_string.copy() + params['parsed_body'] = parser.parsed_body + params['expr_ids'] = parser.expression_ids + return SimpleEscapeFormatString(**params) if all(qt in parsed_string['body'] for qt in ("'", '"')): # don't transform complicated escape yet return ImmutableString(token_string) @@ -567,10 +563,10 @@ def _main(argv, standard_out, standard_error): parser.add_argument('--f-string-expression-quote', help='quote inside expressions in f-string.', choices=['ignore', 'depended', 'single', 'double'], - default='ignore') + default='depended') parser.add_argument('--version', action='version', version='%(prog)s ' + __version__) - parser.add_argument('files', nargs='+', + parser.add_argument('files', nargs='+', metavar='file', help='files to format') args = parser.parse_args(argv[1:]) From 45abb9345848de25ffcddcb8018cf6473167b3d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Tue, 3 Nov 2020 16:15:10 +0500 Subject: [PATCH 16/17] add low python version compability. --- test_unify.py | 3 +++ unify.py | 9 ++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/test_unify.py b/test_unify.py index 9dab5b1..c11c2f1 100755 --- a/test_unify.py +++ b/test_unify.py @@ -6,6 +6,7 @@ import contextlib import io +import sys import tempfile from textwrap import dedent @@ -131,6 +132,7 @@ def test_backslash_train(self): self.assertEqual(result, '''"\\\\'a"''') +@unittest.skipIf(sys.version_info < (3, 6), 'Low python version') class TestUnitsSimpleQuotedFormatString(unittest.TestCase): def test_no_quote_in_expression_area(self): @@ -274,6 +276,7 @@ def test_no_change(self): self.assertEqual(result, '''b"""foo"""''') +@unittest.skipIf(sys.version_info < (3, 6), 'Low python version') class TestFstringParser(unittest.TestCase): def test_find_expression_areas(self): diff --git a/unify.py b/unify.py index 15bed59..e816b70 100755 --- a/unify.py +++ b/unify.py @@ -452,11 +452,10 @@ def get_editable_string(token_type, token_string): text_has_single = parser.text_has_single_quote() text_has_double = parser.text_has_double_quote() if not text_has_single and not text_has_double: - return SimpleEscapeFormatString( - **parsed_string, - parsed_body=parser.parsed_body, - expr_ids=parser.expression_ids - ) + params = parsed_string.copy() + params['parsed_body'] = parser.parsed_body + params['expr_ids'] = parser.expression_ids + return SimpleEscapeFormatString(**params) return ImmutableString(token_string) if 'f' in parsed_string['prefix'].lower(): parser = FstringParser(parsed_string['body']) From dfa5f87e586e1a16f77ffe1fa64cadbc9f7c6ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B5=D0=BA=D1=87=D1=83=D1=80=D0=B8=D0=BD=20=D0=92?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2?= Date: Tue, 3 Nov 2020 16:35:41 +0500 Subject: [PATCH 17/17] tox.ini fixed. add py37 suport, drop py26 support. --- .travis.yml | 1 + setup.py | 1 - test_unify.py | 7 +------ tox.ini | 5 +++-- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5112442..ac9ac2e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - "3.4" - "3.5" - "3.6" + - "3.7" install: - python setup.py --quiet install diff --git a/setup.py b/setup.py index 51a48e7..b3b6aef 100755 --- a/setup.py +++ b/setup.py @@ -28,7 +28,6 @@ def version(): url='https://github.com/myint/unify', classifiers=['Intended Audience :: Developers', 'Environment :: Console', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'License :: OSI Approved :: MIT License'], diff --git a/test_unify.py b/test_unify.py index c11c2f1..4dbfd78 100755 --- a/test_unify.py +++ b/test_unify.py @@ -9,12 +9,7 @@ import sys import tempfile from textwrap import dedent - -try: - # Python 2.6 - import unittest2 as unittest -except ImportError: - import unittest +import unittest import unify diff --git a/tox.ini b/tox.ini index c49dc8f..5b9ff4b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] -envlist = py2.6,py2.7,py3.4,py3.5,py3.6 +envlist = py27,py34,py35,py36,py37 [testenv] -commands=python setup.py test +commands = + python test_unify.py