From d333e3c419f48f4b2d96528cb4c83db1f02adaec Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 18 Nov 2021 19:02:04 -0500 Subject: [PATCH] drop python 2.x --- .pre-commit-config.yaml | 2 + README.rst | 2 +- azure-pipelines.yml | 13 +---- bin/build-manylinux-wheels | 6 +-- docs/conf.py | 17 +++--- docs/index.rst | 2 +- pysassc.py | 8 ++- sass.py | 87 +++++++++++++------------------ sasstests.py | 103 ++++++++++++++++++------------------- sassutils/_compat.py | 7 --- sassutils/builder.py | 28 +++++----- sassutils/distutils.py | 1 - sassutils/wsgi.py | 9 ++-- setup.py | 13 ++--- tox.ini | 2 +- 15 files changed, 129 insertions(+), 171 deletions(-) delete mode 100644 sassutils/_compat.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ba717119..5c782a74 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,9 @@ repos: rev: v2.29.0 hooks: - id: pyupgrade + args: [--py36-plus] - repo: https://github.com/asottile/add-trailing-comma rev: v2.2.0 hooks: - id: add-trailing-comma + args: [--py36-plus] diff --git a/README.rst b/README.rst index e11625d8..9345f4bd 100644 --- a/README.rst +++ b/README.rst @@ -24,7 +24,7 @@ distribution/deployment. That means you can add just ``libsass`` into your ``setup.py``'s ``install_requires`` list or ``requirements.txt`` file. No need for Ruby nor Node.js. -It currently supports CPython 2.7, 3.6--3.8, and PyPy 2.3+! +It currently supports CPython 3.6+, and PyPy 3! .. _Sass: https://sass-lang.com/ .. _LibSass: https://github.com/sass/libsass diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ac189740..60895331 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -18,18 +18,9 @@ resources: jobs: - template: job--python-tox.yml@asottile parameters: - toxenvs: [py27, py36] + toxenvs: [py36] os: macos wheel_tags: true -- template: job--python-tox.yml@asottile - parameters: - toxenvs: [py27] - os: windows - architectures: [x64, x86] - name_postfix: _py27 - wheel_tags: true - pre_test: - - script: rm -rf libsass/test - template: job--python-tox.yml@asottile parameters: toxenvs: [py36] @@ -38,5 +29,5 @@ jobs: wheel_tags: true - template: job--python-tox.yml@asottile parameters: - toxenvs: [pypy, pypy3, py27, py36, py37, py38, py39] + toxenvs: [pypy3, py36, py37, py38, py39] os: linux diff --git a/bin/build-manylinux-wheels b/bin/build-manylinux-wheels index c0f7fc63..70958b43 100755 --- a/bin/build-manylinux-wheels +++ b/bin/build-manylinux-wheels @@ -23,13 +23,13 @@ def main(): os.makedirs('dist', exist_ok=True) for python in ('cp27-cp27mu', 'cp36-cp36m'): with tempfile.TemporaryDirectory() as work: - pip = '/opt/python/{}/bin/pip'.format(python) + pip = f'/opt/python/{python}/bin/pip' check_call( 'docker', 'run', '-ti', # Use this so the files are not owned by root - '--user', '{}:{}'.format(os.getuid(), os.getgid()), + '--user', f'{os.getuid()}:{os.getgid()}', # We'll do building in /work and copy results to /dist - '-v', '{}:/work:rw'.format(work), + '-v', f'{work}:/work:rw', '-v', '{}:/dist:rw'.format(os.path.abspath('dist')), 'quay.io/pypa/manylinux1_x86_64:latest', 'bash', '-exc', diff --git a/docs/conf.py b/docs/conf.py index 921d65c8..f0f0d617 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # libsass documentation build configuration file, created by # sphinx-quickstart on Sun Aug 19 22:45:57 2012. @@ -48,8 +47,8 @@ master_doc = 'index' # General information about the project. -project = u'libsass' -copyright = u'2012, Hong Minhee' +project = 'libsass' +copyright = '2012, Hong Minhee' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -194,8 +193,8 @@ # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( - 'index', 'libsass.tex', u'libsass Documentation', - u'Hong Minhee', 'manual', + 'index', 'libsass.tex', 'libsass Documentation', + 'Hong Minhee', 'manual', ), ] @@ -226,8 +225,8 @@ # (source start file, name, description, authors, manual section). man_pages = [ ( - 'index', 'libsass', u'libsass Documentation', - [u'Hong Minhee'], 1, + 'index', 'libsass', 'libsass Documentation', + ['Hong Minhee'], 1, ), ] @@ -242,8 +241,8 @@ # dir menu entry, description, category) texinfo_documents = [ ( - 'index', 'libsass', u'libsass Documentation', - u'Hong Minhee', 'libsass', 'One line description of project.', + 'index', 'libsass', 'libsass Documentation', + 'Hong Minhee', 'libsass', 'One line description of project.', 'Miscellaneous', ), ] diff --git a/docs/index.rst b/docs/index.rst index 2613f792..e571b487 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,7 +8,7 @@ distribution/deployment. That means you can add just ``libsass`` into your :file:`setup.py`'s ``install_requires`` list or :file:`requirements.txt` file. -It currently supports CPython 2.6, 2.7, 3.5--3.7, and PyPy 2.3+! +It currently supports CPython 3.6+ and PyPy 3! .. _Sass: https://sass-lang.com/ .. _LibSass: https://github.com/sass/libsass diff --git a/pysassc.py b/pysassc.py index b3ef1b0f..aa5a91d6 100755 --- a/pysassc.py +++ b/pysassc.py @@ -88,10 +88,8 @@ .. _SassC: https://github.com/sass/sassc """ -from __future__ import print_function import functools -import io import optparse import sys import warnings @@ -219,7 +217,7 @@ def main(argv=sys.argv, stdout=sys.stdout, stderr=sys.stderr): include_paths=options.include_paths, precision=options.precision, ) - except (IOError, OSError) as e: + except OSError as e: error(e) return 3 except sass.CompileError as e: @@ -229,10 +227,10 @@ def main(argv=sys.argv, stdout=sys.stdout, stderr=sys.stderr): if len(args) < 2: print(css, file=stdout) else: - with io.open(args[1], 'w', encoding='utf-8', newline='') as f: + with open(args[1], 'w', encoding='utf-8', newline='') as f: f.write(css) if source_map_filename: - with io.open( + with open( source_map_filename, 'w', encoding='utf-8', newline='', ) as f: f.write(source_map) diff --git a/sass.py b/sass.py index 8bc0dd88..86992e68 100644 --- a/sass.py +++ b/sass.py @@ -10,21 +10,15 @@ 'a b {\n color: blue; }\n' """ -from __future__ import absolute_import -import collections +import collections.abc import inspect -import io -import os import os.path import re import sys import warnings -from six import string_types, text_type, PY2 - import _sass -from sassutils._compat import collections_abc __all__ = ( 'MODES', 'OUTPUT_STYLES', 'SOURCE_COMMENTS', 'CompileError', 'SassColor', @@ -52,11 +46,10 @@ def to_native_s(s): - if isinstance(s, bytes) and not PY2: # pragma: no cover (py3) - s = s.decode('UTF-8') - elif isinstance(s, text_type) and PY2: # pragma: no cover (py2) - s = s.encode('UTF-8') - return s + if isinstance(s, bytes): + return s.decode('UTF-8') + else: + return s class CompileError(ValueError): @@ -64,7 +57,7 @@ class CompileError(ValueError): It is a subtype of :exc:`exceptions.ValueError`. """ def __init__(self, msg): - super(CompileError, self).__init__(to_native_s(msg)) + super().__init__(to_native_s(msg)) def mkdirp(path): @@ -76,7 +69,7 @@ def mkdirp(path): raise -class SassFunction(object): +class SassFunction: """Custom function for Sass. It can be instantiated using :meth:`from_lambda()` and :meth:`from_named_function()` as well. @@ -107,16 +100,10 @@ def from_lambda(cls, name, lambda_): :rtype: :class:`SassFunction` """ - if PY2: # pragma: no cover - a = inspect.getargspec(lambda_) - varargs, varkw, defaults, kwonlyargs = ( - a.varargs, a.keywords, a.defaults, None, - ) - else: # pragma: no cover - a = inspect.getfullargspec(lambda_) - varargs, varkw, defaults, kwonlyargs = ( - a.varargs, a.varkw, a.defaults, a.kwonlyargs, - ) + a = inspect.getfullargspec(lambda_) + varargs, varkw, defaults, kwonlyargs = ( + a.varargs, a.varkw, a.defaults, a.kwonlyargs, + ) if varargs or varkw or defaults or kwonlyargs: raise TypeError( @@ -142,9 +129,9 @@ def from_named_function(cls, function): return cls.from_lambda(function.__name__, function) def __init__(self, name, arguments, callable_): - if not isinstance(name, string_types): + if not isinstance(name, str): raise TypeError('name must be a string, not ' + repr(name)) - elif not isinstance(arguments, collections_abc.Sequence): + elif not isinstance(arguments, collections.abc.Sequence): raise TypeError( 'arguments must be a sequence, not ' + repr(arguments), @@ -263,7 +250,7 @@ def compile_dirname( if s: v = v.decode('UTF-8') mkdirp(os.path.dirname(output_filename)) - with io.open( + with open( output_filename, 'w', encoding='UTF-8', newline='', ) as output_file: output_file.write(v) @@ -277,7 +264,7 @@ def _check_no_remaining_kwargs(func, kwargs): raise TypeError( '{}() got unexpected keyword argument(s) {}'.format( func.__name__, - ', '.join("'{}'".format(arg) for arg in sorted(kwargs)), + ', '.join(f"'{arg}'" for arg in sorted(kwargs)), ), ) @@ -563,7 +550,7 @@ def my_importer(path, prev): ) precision = kwargs.pop('precision', 5) output_style = kwargs.pop('output_style', 'nested') - if not isinstance(output_style, string_types): + if not isinstance(output_style, str): raise TypeError( 'output_style must be a string, not ' + repr(output_style), @@ -612,9 +599,9 @@ def my_importer(path, prev): def _get_file_arg(key): ret = kwargs.pop(key, None) - if ret is not None and not isinstance(ret, string_types): - raise TypeError('{} must be a string, not {!r}'.format(key, ret)) - elif isinstance(ret, text_type): + if ret is not None and not isinstance(ret, str): + raise TypeError(f'{key} must be a string, not {ret!r}') + elif isinstance(ret, str): ret = ret.encode(fs_encoding) if ret and 'filename' not in modes: raise CompileError( @@ -631,25 +618,25 @@ def _get_file_arg(key): omit_source_map_url = kwargs.pop('omit_source_map_url', False) source_map_root = kwargs.pop('source_map_root', None) - if isinstance(source_map_root, text_type): + if isinstance(source_map_root, str): source_map_root = source_map_root.encode('utf-8') # #208: cwd is always included in include paths include_paths = (os.getcwd(),) include_paths += tuple(kwargs.pop('include_paths', ()) or ()) include_paths = os.pathsep.join(include_paths) - if isinstance(include_paths, text_type): + if isinstance(include_paths, str): include_paths = include_paths.encode(fs_encoding) custom_functions = kwargs.pop('custom_functions', ()) - if isinstance(custom_functions, collections_abc.Mapping): + if isinstance(custom_functions, collections.abc.Mapping): custom_functions = [ SassFunction.from_lambda(name, lambda_) for name, lambda_ in custom_functions.items() ] elif isinstance( custom_functions, - (collections_abc.Set, collections_abc.Sequence), + (collections.abc.Set, collections.abc.Sequence), ): custom_functions = [ func if isinstance(func, SassFunction) @@ -676,7 +663,7 @@ def _get_file_arg(key): if 'string' in modes: string = kwargs.pop('string') - if isinstance(string, text_type): + if isinstance(string, str): string = string.encode('utf-8') indented = kwargs.pop('indented', False) if not isinstance(indented, bool): @@ -695,11 +682,11 @@ def _get_file_arg(key): return v.decode('utf-8') elif 'filename' in modes: filename = kwargs.pop('filename') - if not isinstance(filename, string_types): + if not isinstance(filename, str): raise TypeError('filename must be a string, not ' + repr(filename)) elif not os.path.isfile(filename): - raise IOError('{!r} seems not a file'.format(filename)) - elif isinstance(filename, text_type): + raise OSError(f'{filename!r} seems not a file') + elif isinstance(filename, str): filename = filename.encode(fs_encoding) _check_no_remaining_kwargs(compile, kwargs) s, v, source_map = _sass.compile_filename( @@ -780,9 +767,9 @@ class SassNumber(collections.namedtuple('SassNumber', ('value', 'unit'))): def __new__(cls, value, unit): value = float(value) - if not isinstance(unit, text_type): + if not isinstance(unit, str): unit = unit.decode('UTF-8') - return super(SassNumber, cls).__new__(cls, value, unit) + return super().__new__(cls, value, unit) class SassColor(collections.namedtuple('SassColor', ('r', 'g', 'b', 'a'))): @@ -792,7 +779,7 @@ def __new__(cls, r, g, b, a): g = float(g) b = float(b) a = float(a) - return super(SassColor, cls).__new__(cls, r, g, b, a) + return super().__new__(cls, r, g, b, a) SASS_SEPARATOR_COMMA = collections.namedtuple('SASS_SEPARATOR_COMMA', ())() @@ -810,26 +797,26 @@ def __new__(cls, items, separator, bracketed=False): items = tuple(items) assert separator in SEPARATORS, separator assert isinstance(bracketed, bool), bracketed - return super(SassList, cls).__new__(cls, items, separator, bracketed) + return super().__new__(cls, items, separator, bracketed) class SassError(collections.namedtuple('SassError', ('msg',))): def __new__(cls, msg): - if not isinstance(msg, text_type): + if not isinstance(msg, str): msg = msg.decode('UTF-8') - return super(SassError, cls).__new__(cls, msg) + return super().__new__(cls, msg) class SassWarning(collections.namedtuple('SassWarning', ('msg',))): def __new__(cls, msg): - if not isinstance(msg, text_type): + if not isinstance(msg, str): msg = msg.decode('UTF-8') - return super(SassWarning, cls).__new__(cls, msg) + return super().__new__(cls, msg) -class SassMap(collections_abc.Mapping): +class SassMap(collections.abc.Mapping): """Because sass maps can have mapping types as keys, we need an immutable hashable mapping type. @@ -858,7 +845,7 @@ def __len__(self): # Our interface def __repr__(self): - return '{}({})'.format(type(self).__name__, frozenset(self.items())) + return f'{type(self).__name__}({frozenset(self.items())})' def __hash__(self): return self._hash diff --git a/sasstests.py b/sasstests.py index 6aaf5539..c901e561 100644 --- a/sasstests.py +++ b/sasstests.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8 -*- - import base64 +import collections.abc import contextlib import functools import glob -import json import io +import json import os import os.path import re @@ -17,14 +16,12 @@ import unittest import pytest -from six import StringIO, b, string_types, text_type from werkzeug.test import Client from werkzeug.wrappers import Response import pysassc import sass import sassc -from sassutils._compat import collections_abc from sassutils.builder import Manifest, build_directory from sassutils.wsgi import SassMiddleware @@ -71,7 +68,7 @@ def set_coverage_instrumentation(): ), } -with io.open('test/a.scss', newline='') as f: +with open('test/a.scss', newline='') as f: A_EXPECTED_MAP_CONTENTS = dict(A_EXPECTED_MAP, sourcesContent=[f.read()]) B_EXPECTED_CSS = '''\ @@ -95,7 +92,7 @@ def set_coverage_instrumentation(): color: green; } ''' -D_EXPECTED_CSS = u'''\ +D_EXPECTED_CSS = '''\ @charset "UTF-8"; body { background-color: green; } @@ -103,7 +100,7 @@ def set_coverage_instrumentation(): font: '나눔고딕', sans-serif; } ''' -D_EXPECTED_CSS_WITH_MAP = u'''\ +D_EXPECTED_CSS_WITH_MAP = '''\ @charset "UTF-8"; body { background-color: green; } @@ -149,7 +146,7 @@ def set_coverage_instrumentation(): def _map_in_output_dir(s): def cb(match): filename = os.path.basename(match.group(1)) - return '/*# sourceMappingURL={} */'.format(filename) + return f'/*# sourceMappingURL={filename} */' return re_sourcemap_url.sub(cb, s) @@ -163,9 +160,9 @@ def no_warnings(recwarn): class BaseTestCase(unittest.TestCase): def assert_source_map_equal(self, expected, actual): - if isinstance(expected, string_types): + if isinstance(expected, str): expected = json.loads(expected) - if isinstance(actual, string_types): + if isinstance(actual, str): actual = json.loads(actual) assert expected == actual @@ -175,7 +172,7 @@ def assert_source_map_file(self, expected, filename): tree = json.load(f) except ValueError as e: # pragma: no cover f.seek(0) - msg = '{!s}\n\n{}:\n\n{}'.format(e, filename, f.read()) + msg = f'{e!s}\n\n{filename}:\n\n{f.read()}' raise ValueError(msg) self.assert_source_map_equal(expected, tree) @@ -196,7 +193,7 @@ def test_version(self): assert re.match(r'^\d+\.\d+\.\d+$', sass.__version__) def test_output_styles(self): - assert isinstance(sass.OUTPUT_STYLES, collections_abc.Mapping) + assert isinstance(sass.OUTPUT_STYLES, collections.abc.Mapping) assert 'nested' in sass.OUTPUT_STYLES def test_and_join(self): @@ -294,9 +291,9 @@ def test_compile_string(self): a b { color: blue; } ''' - actual = sass.compile(string=u'a { color: blue; } /* 유니코드 */') + actual = sass.compile(string='a { color: blue; } /* 유니코드 */') self.assertEqual( - u'''@charset "UTF-8"; + '''@charset "UTF-8"; a { color: blue; } @@ -330,11 +327,11 @@ def test_compile_file_sass_style(self): def test_importer_one_arg(self): """Demonstrates one-arg importers + chaining.""" def importer_returning_one_argument(path): - assert type(path) is text_type + assert type(path) is str return ( # Trigger the import of an actual file ('test/b.scss',), - (path, '.{0}-one-arg {{ color: blue; }}'.format(path)), + (path, f'.{path}-one-arg {{ color: blue; }}'), ) ret = sass.compile( @@ -448,7 +445,7 @@ def importer_with_srcmap(path): def test_importers_raises_exception(self): def importer(path): - raise ValueError('Bad path: {}'.format(path)) + raise ValueError(f'Bad path: {path}') with assert_raises_compile_error( RegexMatcher( @@ -640,31 +637,31 @@ def test_builder_build_directory(self): result_files = build_directory(self.sass_path, css_path) assert len(result_files) == 8 assert 'a.scss.css' == result_files['a.scss'] - with io.open( + with open( os.path.join(css_path, 'a.scss.css'), encoding='UTF-8', ) as f: css = f.read() assert A_EXPECTED_CSS == css assert 'b.scss.css' == result_files['b.scss'] - with io.open( + with open( os.path.join(css_path, 'b.scss.css'), encoding='UTF-8', ) as f: css = f.read() assert B_EXPECTED_CSS == css assert 'c.scss.css' == result_files['c.scss'] - with io.open( + with open( os.path.join(css_path, 'c.scss.css'), encoding='UTF-8', ) as f: css = f.read() assert C_EXPECTED_CSS == css assert 'd.scss.css' == result_files['d.scss'] - with io.open( + with open( os.path.join(css_path, 'd.scss.css'), encoding='UTF-8', ) as f: css = f.read() assert D_EXPECTED_CSS == css assert 'e.scss.css' == result_files['e.scss'] - with io.open( + with open( os.path.join(css_path, 'e.scss.css'), encoding='UTF-8', ) as f: css = f.read() @@ -673,7 +670,7 @@ def test_builder_build_directory(self): os.path.join('subdir', 'recur.scss.css'), result_files[os.path.join('subdir', 'recur.scss')], ) - with io.open( + with open( os.path.join(css_path, 'g.scss.css'), encoding='UTF-8', ) as f: css = f.read() @@ -683,12 +680,12 @@ def test_builder_build_directory(self): result_files[os.path.join('subdir', 'recur.scss')], ) assert 'h.sass.css' == result_files['h.sass'] - with io.open( + with open( os.path.join(css_path, 'h.sass.css'), encoding='UTF-8', ) as f: css = f.read() assert H_EXPECTED_CSS == css - with io.open( + with open( os.path.join(css_path, 'subdir', 'recur.scss.css'), encoding='UTF-8', ) as f: @@ -703,7 +700,7 @@ def test_output_style(self): ) assert len(result_files) == 8 assert 'a.scss.css' == result_files['a.scss'] - with io.open( + with open( os.path.join(css_path, 'a.scss.css'), encoding='UTF-8', ) as f: css = f.read() @@ -755,7 +752,7 @@ def test_build_one(self): with open(os.path.join(d, 'css', 'a.scss.css')) as f: assert A_EXPECTED_CSS == f.read() m.build_one(d, 'b.scss', source_map=True) - with io.open( + with open( os.path.join(d, 'css', 'b.scss.css'), encoding='UTF-8', ) as f: assert f.read() == _map_in_output_dir(B_EXPECTED_CSS_WITH_MAP) @@ -773,7 +770,7 @@ def test_build_one(self): os.path.join(d, 'css', 'b.scss.css.map'), ) m.build_one(d, 'd.scss', source_map=True) - with io.open( + with open( os.path.join(d, 'css', 'd.scss.css'), encoding='UTF-8', ) as f: assert f.read() == _map_in_output_dir(D_EXPECTED_CSS_WITH_MAP) @@ -838,7 +835,7 @@ def test_wsgi_sass_middleware(self): r = client.get('/static/a.scss.css') assert r.status_code == 200 self.assertEqual( - b(_map_in_output_dir(A_EXPECTED_CSS_WITH_MAP)), + _map_in_output_dir(A_EXPECTED_CSS_WITH_MAP).encode(), r.data, ) assert r.mimetype == 'text/css' @@ -903,7 +900,7 @@ def css_path(self, *args): return os.path.join( os.path.dirname(__file__), 'testpkg', 'testpkg', 'static', 'css', - *args + *args, ) def list_built_css(self): @@ -942,8 +939,8 @@ def test_output_style(self): class SasscTestCase(BaseTestCase): def setUp(self): - self.out = StringIO() - self.err = StringIO() + self.out = io.StringIO() + self.err = io.StringIO() def test_no_args(self): exit_code = pysassc.main(['pysassc'], self.out, self.err) @@ -995,7 +992,7 @@ def test_pysassc_output(self): assert exit_code == 0 assert self.err.getvalue() == '' assert self.out.getvalue() == '' - with io.open(tmp, encoding='UTF-8', newline='') as f: + with open(tmp, encoding='UTF-8', newline='') as f: assert A_EXPECTED_CSS.strip() == f.read().strip() finally: os.remove(tmp) @@ -1011,7 +1008,7 @@ def test_pysassc_output_unicode(self): assert exit_code == 0 assert self.err.getvalue() == '' assert self.out.getvalue() == '' - with io.open(tmp, encoding='UTF-8') as f: + with open(tmp, encoding='UTF-8') as f: assert D_EXPECTED_CSS.strip() == f.read().strip() finally: os.remove(tmp) @@ -1093,10 +1090,10 @@ def test_compile_directories_unicode(self): input_dir = os.path.join(tmpdir, 'input') output_dir = os.path.join(tmpdir, 'output') os.makedirs(input_dir) - with io.open( + with open( os.path.join(input_dir, 'test.scss'), 'w', encoding='UTF-8', ) as f: - f.write(u'a { content: "☃"; }') + f.write('a { content: "☃"; }') # Raised a UnicodeEncodeError in py2 before #82 (issue #72) # Also raised a UnicodeEncodeError in py3 if the default encoding # couldn't represent it (such as cp1252 on windows) @@ -1164,14 +1161,14 @@ def test_sass_func_type_errors(func): class SassTypesTest(unittest.TestCase): def test_number_no_conversion(self): - num = sass.SassNumber(123., u'px') + num = sass.SassNumber(123., 'px') assert type(num.value) is float, type(num.value) - assert type(num.unit) is text_type, type(num.unit) + assert type(num.unit) is str, type(num.unit) def test_number_conversion(self): num = sass.SassNumber(123, b'px') assert type(num.value) is float, type(num.value) - assert type(num.unit) is text_type, type(num.unit) + assert type(num.unit) is str, type(num.unit) def test_color_no_conversion(self): color = sass.SassColor(1., 2., 3., .5) @@ -1198,20 +1195,20 @@ def test_sass_list_conversion(self): assert lst.separator is sass.SASS_SEPARATOR_SPACE, lst.separator def test_sass_warning_no_conversion(self): - warn = sass.SassWarning(u'error msg') - assert type(warn.msg) is text_type, type(warn.msg) + warn = sass.SassWarning('error msg') + assert type(warn.msg) is str, type(warn.msg) def test_sass_warning_no_conversion_bytes_message(self): warn = sass.SassWarning(b'error msg') - assert type(warn.msg) is text_type, type(warn.msg) + assert type(warn.msg) is str, type(warn.msg) def test_sass_error_no_conversion(self): - err = sass.SassError(u'error msg') - assert type(err.msg) is text_type, type(err.msg) + err = sass.SassError('error msg') + assert type(err.msg) is str, type(err.msg) def test_sass_error_conversion(self): err = sass.SassError(b'error msg') - assert type(err.msg) is text_type, type(err.msg) + assert type(err.msg) is str, type(err.msg) def raises(): @@ -1244,11 +1241,11 @@ def returns_none(): def returns_unicode(): - return u'☃' + return '☃' def returns_bytes(): - return u'☃'.encode('UTF-8') + return '☃'.encode() def returns_number(): @@ -1380,7 +1377,7 @@ def assert_raises_compile_error(expected): assert msg == expected, (msg, expected) -class RegexMatcher(object): +class RegexMatcher: def __init__(self, reg, flags=None): self.reg = re.compile(reg, re.MULTILINE | re.DOTALL) @@ -1472,13 +1469,13 @@ def test_false(self): def test_unicode(self): self.assertEqual( compile_with_func('a { content: returns_unicode(); }'), - u'\ufeffa{content:☃}\n', + '\ufeffa{content:☃}\n', ) def test_bytes(self): self.assertEqual( compile_with_func('a { content: returns_bytes(); }'), - u'\ufeffa{content:☃}\n', + '\ufeffa{content:☃}\n', ) def test_number(self): @@ -1550,7 +1547,7 @@ def test_identity_false(self): def test_identity_strings(self): self.assertEqual( compile_with_func('a { content: identity(returns_unicode()); }'), - u'\ufeffa{content:☃}\n', + '\ufeffa{content:☃}\n', ) def test_identity_number(self): @@ -1626,7 +1623,7 @@ def test_map_with_map_key(self): def test_stack_trace_formatting(): try: - sass.compile(string=u'a{☃') + sass.compile(string='a{☃') raise AssertionError('expected to raise CompileError') except sass.CompileError: tb = traceback.format_exc() diff --git a/sassutils/_compat.py b/sassutils/_compat.py deleted file mode 100644 index 8f794ac8..00000000 --- a/sassutils/_compat.py +++ /dev/null @@ -1,7 +0,0 @@ -from six import PY2 - - -if PY2: # pragma: no cover (PY2) - import collections as collections_abc # noqa: F401 -else: # pragma: no cover (PY3) - import collections.abc as collections_abc # noqa: F401 diff --git a/sassutils/builder.py b/sassutils/builder.py index b07dfad7..747b5b40 100644 --- a/sassutils/builder.py +++ b/sassutils/builder.py @@ -3,16 +3,12 @@ """ -import io -import os +import collections.abc import os.path import re import warnings -from six import string_types - from sass import compile -from sassutils._compat import collections_abc __all__ = 'SUFFIXES', 'SUFFIX_PATTERN', 'Manifest', 'build_directory' @@ -69,7 +65,7 @@ def build_directory( output_style=output_style, include_paths=[_root_sass], ) - with io.open( + with open( css_fullname, 'w', encoding='utf-8', newline='', ) as css_file: css_file.write(css) @@ -88,7 +84,7 @@ def build_directory( return result -class Manifest(object): +class Manifest: """Building manifest of Sass/SCSS. :param sass_path: the path of the directory that contains Sass/SCSS @@ -105,7 +101,7 @@ class Manifest(object): def normalize_manifests(cls, manifests): if manifests is None: manifests = {} - elif isinstance(manifests, collections_abc.Mapping): + elif isinstance(manifests, collections.abc.Mapping): manifests = dict(manifests) else: raise TypeError( @@ -113,7 +109,7 @@ def normalize_manifests(cls, manifests): repr(manifests), ) for package_name, manifest in manifests.items(): - if not isinstance(package_name, string_types): + if not isinstance(package_name, str): raise TypeError( 'manifest keys must be a string of package ' 'name, not ' + repr(package_name), @@ -122,9 +118,9 @@ def normalize_manifests(cls, manifests): continue elif isinstance(manifest, tuple): manifest = Manifest(*manifest) - elif isinstance(manifest, collections_abc.Mapping): + elif isinstance(manifest, collections.abc.Mapping): manifest = Manifest(**manifest) - elif isinstance(manifest, string_types): + elif isinstance(manifest, str): manifest = Manifest(manifest) else: raise TypeError( @@ -142,21 +138,21 @@ def __init__( wsgi_path=None, strip_extension=None, ): - if not isinstance(sass_path, string_types): + if not isinstance(sass_path, str): raise TypeError( 'sass_path must be a string, not ' + repr(sass_path), ) if css_path is None: css_path = sass_path - elif not isinstance(css_path, string_types): + elif not isinstance(css_path, str): raise TypeError( 'css_path must be a string, not ' + repr(css_path), ) if wsgi_path is None: wsgi_path = css_path - elif not isinstance(wsgi_path, string_types): + elif not isinstance(wsgi_path, str): raise TypeError( 'wsgi_path must be a string, not ' + repr(wsgi_path), @@ -292,11 +288,11 @@ def build_one(self, package_dir, filename, source_map=False): css_folder = os.path.dirname(css_path) if not os.path.exists(css_folder): os.makedirs(css_folder) - with io.open(css_path, 'w', encoding='utf-8', newline='') as f: + with open(css_path, 'w', encoding='utf-8', newline='') as f: f.write(css) if source_map: # Source maps are JSON, and JSON has to be UTF-8 encoded - with io.open( + with open( source_map_path, 'w', encoding='utf-8', newline='', ) as f: f.write(source_map) diff --git a/sassutils/distutils.py b/sassutils/distutils.py index 52b19c67..ab178038 100644 --- a/sassutils/distutils.py +++ b/sassutils/distutils.py @@ -67,7 +67,6 @@ Added ``--output-style``/``-s`` option to :class:`build_sass` command. """ -from __future__ import absolute_import import distutils.errors import distutils.log diff --git a/sassutils/wsgi.py b/sassutils/wsgi.py index 2d8a5543..56c2769c 100644 --- a/sassutils/wsgi.py +++ b/sassutils/wsgi.py @@ -2,8 +2,8 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ """ -from __future__ import absolute_import +import collections.abc import logging import os import os.path @@ -11,13 +11,12 @@ from pkg_resources import resource_filename from sass import CompileError -from sassutils._compat import collections_abc from .builder import Manifest __all__ = 'SassMiddleware', -class SassMiddleware(object): +class SassMiddleware: r"""WSGI middleware for development purpose. Every time a CSS file has requested it finds a matched Sass/SCSS source file and then compiled it into CSS. @@ -100,7 +99,7 @@ def __init__( ) self.app = app self.manifests = Manifest.normalize_manifests(manifests) - if not isinstance(package_dir, collections_abc.Mapping): + if not isinstance(package_dir, collections.abc.Mapping): raise TypeError( 'package_dir must be a mapping object, not ' + repr(package_dir), @@ -138,7 +137,7 @@ def __call__(self, environ, start_response): sass_filename, source_map=True, ) - except (IOError, OSError): + except OSError: break except CompileError as e: logger = logging.getLogger(__name__ + '.SassMiddleware') diff --git a/setup.py b/setup.py index b1eb95a1..2bbf408c 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import ast import atexit import distutils.cmd @@ -82,9 +80,9 @@ def customize_compiler(compiler): libsass_version = libsass_version_file.read().decode('UTF-8').strip() if sys.platform == 'win32': # This looks wrong, but is required for some reason :( - define = r'/DLIBSASS_VERSION="\"{}\""'.format(libsass_version) + define = fr'/DLIBSASS_VERSION="\"{libsass_version}\""' else: - define = '-DLIBSASS_VERSION="{}"'.format(libsass_version) + define = f'-DLIBSASS_VERSION="{libsass_version}"' for directory in ( os.path.join('libsass', 'src'), @@ -184,7 +182,7 @@ def readme(): try: with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as f: return f.read() - except IOError: + except OSError: pass @@ -231,7 +229,7 @@ def run(self): else: class bdist_wheel(wheel.bdist_wheel.bdist_wheel): def finalize_options(self): - self.py_limited_api = 'cp3{}'.format(sys.version_info[1]) + self.py_limited_api = f'cp3{sys.version_info[1]}' super().finalize_options() cmdclass['bdist_wheel'] = bdist_wheel @@ -270,7 +268,6 @@ def finalize_options(self): ['sassc = sassc:main'], ], }, - install_requires=['six'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', @@ -279,7 +276,6 @@ def finalize_options(self): 'Operating System :: OS Independent', 'Programming Language :: C', 'Programming Language :: C++', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', @@ -293,5 +289,6 @@ def finalize_options(self): 'Topic :: Software Development :: Code Generators', 'Topic :: Software Development :: Compilers', ], + python_requires='>=3.6', cmdclass=cmdclass, ) diff --git a/tox.ini b/tox.ini index 38047b9b..f4b84ad0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pypy,pypy3,py27,py36,py37,py38,py39,pre-commit +envlist = pypy3,py36,py37,py38,py39,pre-commit [testenv] usedevelop = true