From 3cc2d29438009237ffd0dfdd14655ffd2b04eeca Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 24 Jan 2025 12:42:28 +0100 Subject: [PATCH 1/8] Remove support for Python 2.7 --- .github/workflows/ci.yml | 90 +++--- CONTRIBUTING.md | 2 +- pscript/__init__.py | 33 +- pscript/functions.py | 2 - pscript/parser0.py | 6 - pscript/stubs.py | 3 - pscript/tests/test_commonast.py | 5 +- pscript/tests/test_parser2.py | 20 +- pscript/tests/test_parser3.py | 4 +- pscript/tests/test_stdlib.py | 10 +- pscript/tests/test_stubs.py | 2 - setup.py | 27 +- tasks/test.py | 5 +- translate_to_legacy.py | 546 -------------------------------- 14 files changed, 81 insertions(+), 674 deletions(-) delete mode 100644 translate_to_legacy.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 310a496d..56ac251e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,80 +2,98 @@ name: CI on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] jobs: - build: + + lint: + name: Linting + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '18' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + - name: Ruff lint + run: | + ruff check --output-format=github . + - name: Ruff format + run: | + ruff format --check . + + docs: + name: Docs + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.13 + - name: Install dev dependencies + run: | + python -m pip install --upgrade pip + #pip install -U -e .[docs] + pip install sphinx + - name: Build docs + run: | + cd docs + make html SPHINXOPTS="-W --keep-going" + + tests: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} strategy: matrix: include: - - name: Lint - os: ubuntu-latest - pyversion: '3.7' - dolint: 1 - name: Linux py36 os: ubuntu-latest pyversion: '3.6' - tests: 1 - name: Linux py37 os: ubuntu-latest pyversion: '3.7' - tests: 1 - name: Linux py38 os: ubuntu-latest pyversion: '3.8' - tests: 1 - name: Linux py39 os: ubuntu-latest pyversion: '3.9' - tests: 1 - name: Linux pypy3 os: ubuntu-latest pyversion: 'pypy3' - tests: 1 - name: Windows py38 os: windows-latest pyversion: '3.8' - tests: 1 - name: MacOS py38 os: macos-latest pyversion: '3.8' - tests: 1 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.pyversion }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.pyversion }} - - uses: actions/setup-node@v2 - if: matrix.dolint == 1 - with: - node-version: '14' - - name: Install dependencies (lint and docs) - if: matrix.dolint == 1 - run: | - python -m pip install --upgrade pip - pip install invoke pycodestyle flake8 sphinx - - name: Install dependencies (unit tests) - if: matrix.tests == 1 + - name: Install dependencies for unit tests run: | python -m pip install --upgrade pip pip install invoke pytest pytest-cov - - name: Lint - if: matrix.dolint == 1 - run: | - invoke test --style - - name: Build docs - if: matrix.dolint == 1 - run: | - invoke docs --clean --build - name: Test with pytest - if: matrix.tests == 1 run: | invoke test --unit diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 20cfe120..055cb01f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1 @@ -See https://github.com/flexxui/flexx/blob/master/CONTRIBUTING.md +See https://github.com/flexxui/flexx/blob/main/CONTRIBUTING.md diff --git a/pscript/__init__.py b/pscript/__init__.py index a069473d..86ca574c 100644 --- a/pscript/__init__.py +++ b/pscript/__init__.py @@ -246,16 +246,6 @@ import sys import logging -logger = logging.getLogger(__name__) - -# Assert compatibility and redirect to legacy version on Python 2.7 -ok = True -if sys.version_info[0] == 2: # pragma: no cover - if sys.version_info < (2, 7): - raise RuntimeError('PScript needs at least Python 2.7') - if type(b'') == type(''): # noqa - will be str and unicode after conversion - sys.modules[__name__] = __import__(__name__ + '_legacy') - ok = False # NOTE: The code for the parser is quite long, especially if you want # to document it well. Therefore it is split in multiple modules, which @@ -267,20 +257,19 @@ # demonstrating the features defined in that module. In the docs these # docstrings are combined into one complete guide. -# flake8: noqa -if ok: +from .parser0 import Parser0, JSError +from .parser1 import Parser1 +from .parser2 import Parser2 +from .parser3 import Parser3 +from .base import * - from .parser0 import Parser0, JSError - from .parser1 import Parser1 - from .parser2 import Parser2 - from .parser3 import Parser3 - from .base import * +from .functions import py2js, evaljs, evalpy, JSString +from .functions import script2js, js_rename, create_js_module +from .stdlib import get_full_std_lib, get_all_std_names +from .stubs import RawJS, JSConstant, window, undefined - from .functions import py2js, evaljs, evalpy, JSString - from .functions import script2js, js_rename, create_js_module - from .stdlib import get_full_std_lib, get_all_std_names - from .stubs import RawJS, JSConstant, window, undefined +logger = logging.getLogger(__name__) -del logging, sys, ok +del logging, sys diff --git a/pscript/functions.py b/pscript/functions.py index 135c3cdf..5d4d6b57 100644 --- a/pscript/functions.py +++ b/pscript/functions.py @@ -279,8 +279,6 @@ def evaljs(jscode, whitespace=True, print_result=True, extra_nodejs_args=None): filename = None p_or_e = ['-p', '-e'] if print_result else ['-e'] cmd += ['--use_strict'] + p_or_e + [jscode] - if sys.version_info[0] < 3: - cmd = [c.encode('raw_unicode_escape') for c in cmd] # Call node try: diff --git a/pscript/parser0.py b/pscript/parser0.py index bad466bb..e2cf138c 100644 --- a/pscript/parser0.py +++ b/pscript/parser0.py @@ -152,7 +152,6 @@ class Parser0: 'True' : 'true', 'False' : 'false', 'None' : 'null', - 'unicode': 'str', # legacy Py compat 'unichr': 'chr', 'xrange': 'range', 'self': 'this', @@ -208,12 +207,7 @@ def __init__(self, code, pysource=None, indent=0, docstrings=True, self._pysource = str(pysource[0]), int(pysource[1]) elif pysource is not None: logger.warning('Parser ignores pysource; it must be str or (str, int).') - if sys.version_info[0] == 2: - fut = 'from __future__ import unicode_literals, print_function\n' - code = fut + code self._root = ast.parse(code) - if sys.version_info[0] == 2: - self._root.body_nodes.pop(0) # remove that import node we added self._stack = [] self._indent = indent self._dummy_counter = 0 diff --git a/pscript/stubs.py b/pscript/stubs.py index 0d533dd2..fe67c6ff 100644 --- a/pscript/stubs.py +++ b/pscript/stubs.py @@ -58,9 +58,6 @@ def __init__(self, code, _resolve_defining_module=True): raise Exception() except Exception as err: tb = getattr(err, '__traceback__', None) - if tb is None: # Legacy Python 2.x - import sys - _, _, tb = sys.exc_info() self._globals = tb.tb_frame.f_back.f_globals del tb self.__module__ = self._globals['__name__'] diff --git a/pscript/tests/test_commonast.py b/pscript/tests/test_commonast.py index 44213cbe..f8bca9dd 100644 --- a/pscript/tests/test_commonast.py +++ b/pscript/tests/test_commonast.py @@ -23,10 +23,7 @@ def _export_python_sample_ast(): # Get what to export - if sys.version_info > (3, ): - filenames = filename1, filename3 - else: - filenames = filename2, + filenames = filename1, filename3 # Write for filename in filenames: filename_bz2 = filename[:-2] + 'bz2' diff --git a/pscript/tests/test_parser2.py b/pscript/tests/test_parser2.py index b58c8810..f78892eb 100644 --- a/pscript/tests/test_parser2.py +++ b/pscript/tests/test_parser2.py @@ -512,16 +512,14 @@ def test_when_funcs_do_parse_kwargs(self): assert 'kw_values' not in code # We do for keyword only args - if sys.version_info > (3, ): - code = py2js('def foo(a, *, b=1, c="foo"): pass') - assert 'parse_kwargs' in code - assert 'kw_values' in code + code = py2js('def foo(a, *, b=1, c="foo"): pass') + assert 'parse_kwargs' in code + assert 'kw_values' in code # We do for keyword only args and **kwargs - if sys.version_info > (3, ): - code = py2js('def foo(a, *, b=1, c="foo", **d): pass') - assert 'parse_kwargs' in code - assert 'kw_values' in code + code = py2js('def foo(a, *, b=1, c="foo", **d): pass') + assert 'parse_kwargs' in code + assert 'kw_values' in code def test_func1(self): code = py2js(func1) @@ -560,7 +558,6 @@ def test_function_call_default_args(self): assert evalpy(code + 'foo()') == '9' assert evalpy(code + 'd.foo()') == '9' - @skipif(sys.version_info < (3,), reason='no keyword only args in legacy py') def test_function_call_keyword_only_args(self): code = "def foo(*, a=2, b=3, c=4): return a+b+c;\nd = {'foo':foo}\n" assert evalpy(code + 'foo(a=1, b=2, c=3)') == '6' @@ -615,7 +612,6 @@ def test_function_call_args_and_kwargs(self): assert evalpy(code + 'foo(1, 2, 3, **{"b":4})') == '{ b: 4 }' assert evalpy(code + 'foo(a=3, **{"b":4})') == '{ a: 3, b: 4 }' - @skipif(sys.version_info < (3,), reason='no keyword only args in legacy py') def test_function_call_keyword_only_args_and_kwargs(self): code = "def foo(*, a=3, b=4, **x): return repr([a, b]) + repr(x);\nd = {'foo':foo}\n" assert evalpy(code + 'foo(1)') == '[3,4]{}' @@ -790,7 +786,6 @@ def inner(): assert evaljs(py2js(func1)+'func1()') == '2' assert evaljs(py2js(func2)+'func2()') == '3' - @skipif(sys.version_info < (3,), reason='no nonlocal on legacy Python') def test_nonlocal(self): assert py2js('nonlocal foo;foo = 3').strip() == 'foo = 3;' @@ -804,7 +799,6 @@ def inner(): """ assert evaljs(py2js(func3_code)+'func3()') == '3' - @skipif(sys.version_info < (3,), reason='no nonlocal on legacy Python') def test_global_vs_nonlocal(self): js1 = py2js('global foo;foo = 3') js2 = py2js('nonlocal foo;foo = 3') @@ -955,7 +949,7 @@ def addTwo(self): super().addTwo() self.bar += 1 # haha, we add four! def addFour(self): - super(MyClass3, self).add(4) # Use legacy Python syntax + super(MyClass3, self).add(4) # Use older syntax code = py2js(MyClass1) + py2js(MyClass2) + py2js(MyClass3) code += 'var m1=new MyClass1(), m2=new MyClass2(), m3=new MyClass3();' diff --git a/pscript/tests/test_parser3.py b/pscript/tests/test_parser3.py index abcce201..4a15af86 100644 --- a/pscript/tests/test_parser3.py +++ b/pscript/tests/test_parser3.py @@ -446,8 +446,6 @@ def test_no_dict(self): assert evalpy(code + 'foo = Foo(); foo.clear(); foo.bar') == '42' def test_that_all_dict_methods_are_tested(self): - if sys.version_info[0] == 2: - skip('On legacy py, the dict methods are different') tested = set([x.split('_')[1] for x in dir(self) if x.startswith('test_')]) needed = set([x for x in dir(dict) if not x.startswith('_')]) ignore = 'fromkeys' @@ -777,7 +775,7 @@ def test_splitlines(self): assert evalpy(r'"abc\r\ndef".splitlines(True)') == "[ 'abc\\r\\n', 'def' ]" res = repr("X\n\nX\r\rX\r\n\rX\n\r\nX".splitlines(True)).replace(' ', '') - res = res.replace('u"', '"').replace("u'", "'") # arg legacy py + # res = res.replace('u"', '"').replace("u'", "'") # arg legacy py assert nowhitespace(evalpy(r'"X\n\nX\r\rX\r\n\rX\n\r\nX".splitlines(true)')) == res def test_replace(self): diff --git a/pscript/tests/test_stdlib.py b/pscript/tests/test_stdlib.py index 3db3844c..4cfdc2c8 100644 --- a/pscript/tests/test_stdlib.py +++ b/pscript/tests/test_stdlib.py @@ -34,10 +34,7 @@ def test_stdlib_has_all_list_methods(): def test_stdlib_has_all_dict_methods(): method_names = [m for m in dir(dict) if not m.startswith('_')] - if sys.version_info[0] == 2: - ignore = 'fromkeys has_key viewitems viewkeys viewvalues iteritems iterkeys itervalues' - else: - ignore = 'fromkeys' + ignore = 'fromkeys' for name in ignore.split(' '): method_names.remove(name) for method_name in method_names: @@ -45,10 +42,7 @@ def test_stdlib_has_all_dict_methods(): def test_stdlib_has_all_str_methods(): method_names = [m for m in dir(str) if not m.startswith('_')] - if sys.version_info[0] == 2: - ignore = 'encode decode' - else: - ignore = 'encode format_map isprintable maketrans isascii removeprefix removesuffix' + ignore = 'encode format_map isprintable maketrans isascii removeprefix removesuffix' for name in ignore.split(' '): if name in method_names: method_names.remove(name) diff --git a/pscript/tests/test_stubs.py b/pscript/tests/test_stubs.py index c80f5c1c..2d46f64d 100644 --- a/pscript/tests/test_stubs.py +++ b/pscript/tests/test_stubs.py @@ -6,8 +6,6 @@ def test_stubs(): - if sys.version_info[0] == 2: - return # hard from pscript.stubs import window, undefined, omgnotaname assert isinstance(window, pscript.JSConstant) assert isinstance(undefined, pscript.JSConstant) diff --git a/setup.py b/setup.py index 36717269..c59c559b 100644 --- a/setup.py +++ b/setup.py @@ -43,21 +43,6 @@ def package_tree(pkgroot): return subdirs -def copy_for_legacy_python(src_dir, dest_dir): - from translate_to_legacy import LegacyPythonTranslator - # Dirs and files to explicitly not translate - skip = ['tests/python_sample.py', - 'tests/python_sample2.py', - 'tests/python_sample3.py'] - # Make a fresh copy of the package - if os.path.isdir(dest_dir): - shutil.rmtree(dest_dir) - ignore = lambda src, names: [n for n in names if n == '__pycache__'] - shutil.copytree(src_dir, dest_dir, ignore=ignore) - # Translate in-place - LegacyPythonTranslator.translate_dir(dest_dir, skip=skip) - - ## Collect info for setup() THIS_DIR = os.path.dirname(__file__) @@ -70,14 +55,6 @@ def copy_for_legacy_python(src_dir, dest_dir): version, doc = get_version_and_doc(os.path.join(THIS_DIR, name, '__init__.py')) doc = "" # won't render open(os.path.join(THIS_DIR, 'README.md'), "rb").read().decode() -# Support for legacy Python: we install a second package with the -# translated code. We generate that code when we can. We use -# "name_legacy" below in "packages", "package_dir", and "package_data". -name_legacy = name + '_legacy' -if os.path.isfile(os.path.join(THIS_DIR, 'translate_to_legacy.py')): - copy_for_legacy_python(os.path.join(THIS_DIR, name), - os.path.join(THIS_DIR, name_legacy)) - ## Setup @@ -96,8 +73,8 @@ def copy_for_legacy_python(src_dir, dest_dir): platforms='any', provides=[name], install_requires=[], - packages=package_tree(name) + package_tree(name_legacy), - package_dir={name: name, name_legacy: name_legacy}, + packages=package_tree(name), + package_dir={name: name}, # entry_points={'console_scripts': ['pscript = pscript.__main__:main'], }, zip_safe=True, classifiers=[ diff --git a/tasks/test.py b/tasks/test.py index c2518b27..6942baf6 100644 --- a/tasks/test.py +++ b/tasks/test.py @@ -38,11 +38,10 @@ def test_unit(rel_path='.'): except ImportError: sys.exit('Cannot do unit tests, pytest not installed') # Get path to test - py2 = sys.version_info[0] == 2 - rel_path = 'pscript_legacy/' + rel_path if py2 else 'pscript/' + rel_path + rel_path = 'pscript/' + rel_path test_path = os.path.join(ROOT_DIR, rel_path) # Import from installed, or from ROOT_DIR - if py2 or os.getenv('TEST_INSTALL', '').lower() in ('1', 'yes', 'true'): + if os.getenv('TEST_INSTALL', '').lower() in ('1', 'yes', 'true'): if ROOT_DIR in sys.path: sys.path.remove(ROOT_DIR) os.chdir(os.path.expanduser('~')) diff --git a/translate_to_legacy.py b/translate_to_legacy.py deleted file mode 100644 index dac66206..00000000 --- a/translate_to_legacy.py +++ /dev/null @@ -1,546 +0,0 @@ -# -*- coding: utf-8 -*- -# Source: https://github.com/almarklein/translate_to_legacy -# Copyright (c) 2016, Almar Klein - this code is subject to the BSD license -# The parser code and regexes are based on code by Rob Reilink from the -# IEP project. - -""" -Single module to translate Python 3 code to Python 2.7. Write all your -code in Python 3, and convert it to Python 2.7 at install time. -""" - -from __future__ import print_function - -import os -import re - -# List of fixers from lib3to2: absimport annotations bitlength bool -# bytes classdecorator collections dctsetcomp division except features -# fullargspec funcattrs getcwd imports imports2 input int intern -# itertools kwargs memoryview metaclass methodattrs newstyle next -# numliterals open print printfunction raise range reduce setliteral -# str super throw unittest unpacking with - -ALPHANUM = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - -KEYWORDS = set(['False', 'None', 'True', 'and', 'as', 'assert', 'break', - 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', - 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', - 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', - 'try', 'while', 'with', 'yield']) - -# This regexp is used to find the tokens -tokenProg = re.compile( - '(#)|' + # Comment or - '(' + # Begin of string group (group 1) - '[bB]?[uU]?[rR]?' + # Possibly bytes, unicode, raw - '("""|\'\'\'|"|\')' + # String start (triple qoutes first, group 3) - ')|' + # End of string group - '([' + ALPHANUM + '_]+)' # Identifiers/numbers (group 1) or - ) - -# regexps to find the end of a comment or string -endProgs = { - "#": re.compile(r"\r?\n"), - "'": re.compile(r"([^\\])(\\\\)*'"), - '"': re.compile(r'([^\\])(\\\\)*"'), - "'''": re.compile(r"([^\\])(\\\\)*'''"), - '"""': re.compile(r'([^\\])(\\\\)*"""'), - } - - -class CancelTranslation(RuntimeError): - pass # to cancel a translation - - -class Token: - """ A token in the source code. The type of token can be a comment, - string, keyword, number or identifier. It has functionality to get - information on neighboring tokens and neighboring characters. This - should be enough to do all necessary translations. - - If the ``fix`` attribute is set, that string will replace the - current string. - """ - - def __init__(self, total_text, type, start, end): - self.total_text = total_text - self.type = type - self.start = start - self.end = end - self.fix = None - - def __repr__(self): - return '' % self.text - - def find_forward(self, s): - """ Find the position of a character to the right. - """ - return self.total_text.find(s, self.end) - - def find_backward(self, s): - """ Find the position of a character to the left. - """ - return self.total_text.rfind(s, 0, self.start) - - @property - def text(self): - """ The original text of the token. - """ - return self.total_text[self.start:self.end] - - @property - def prev_char(self): - """ The first non-whitespace char to the left of this token - that is still on the same line. - """ - i = self.find_backward('\n') - i = i if i >= 0 else 0 - line = self.total_text[i:self.start] - line = re.sub(r"\s+", '', line) # remove whitespace - return line[-1:] # return single char or empty string - - @property - def next_char(self): - """ Get the first non-whitespace char to the right of this token - that is still on the same line. - """ - i = self.find_forward('\n') - i = i if i >= 0 else len(self.total_text) - line = self.total_text[self.end:i] - line = re.sub(r"\s+", '', line) # remove whitespace - return line[:1] # return single char or empty string - - @property - def indentation(self): - """ The number of chars that the current line uses for indentation. - """ - i = max(0, self.find_backward('\n')) - line1 = self.total_text[i+1:self.start] - line2 = line1.lstrip() - return len(line1) - len(line2) - - @property - def line_tokens(self): - """ All (non-comment) tokens that are on the same line. - """ - i1, i2 = self.find_backward('\n'), self.find_forward('\n') - i1 = i1 if i1 >= 0 else 0 - i2 = i2 if i2 >= 0 else len(self.total_text) - t = self - tokens = [] - while t.prev_token and t.prev_token.start >= i1: - t = t.prev_token - tokens.append(t) - while (t.next_token and t.next_token.end <= i2 and - t.next_token.type != 'comment'): - t = t.next_token - tokens.append(t) - return tokens - - -class BaseTranslator: - """ Translate Python code. One translator instance is used to - translate one file. - """ - - def __init__(self, text): - self._text = text - self._tokens = None - - @property - def tokens(self): - """ The list of tokens. - """ - if self._tokens is None: - self._parse() - return self._tokens - - def _parse(self): - """ Generate tokens by parsing the code. - """ - self._tokens = [] - pos = 0 - - # Find tokens - while True: - token = self._find_next_token(pos) - if token is None: - break - self._tokens.append(token) - pos = token.end - - # Link tokens - if self._tokens: - self._tokens[0].prev_token = None - self._tokens[len(self._tokens)-1].next_token = None - for i in range(0, len(self._tokens)-1): - self._tokens[i].next_token = self._tokens[i+1] - for i in range(1, len(self._tokens)): - self._tokens[i].prev_token = self._tokens[i-1] - - def _find_next_token(self, pos): - """ Returns a token or None if no new tokens can be found. - """ - - text = self._text - - # Init tokens, if pos too large, were done - if pos > len(text): - return None - - # Find the start of the next string or comment - match = tokenProg.search(text, pos) - - if not match: - return None - if match.group(1): - # Comment - start = match.start() - end_match = endProgs['#'].search(text, start+1) - end = end_match.start() if end_match else len(text) - return Token(text, 'comment', start, end) - elif match.group(2) is not None: - # String - we start the search for the end-char(s) at end-1, - # because our regexp has to allow for one char (which is - # not backslash) before the end char(s). - start = match.start() - string_style = match.group(3) - end = endProgs[string_style].search(text, match.end() - 1).end() - return Token(text, 'string', start, end) - else: - # Identifier ("a word or number") Find out whether it is a key word - identifier = match.group(4) - tokenArgs = match.start(), match.end() - if identifier in KEYWORDS: - return Token(text, 'keyword', *tokenArgs) - elif identifier[0] in '0123456789': - return Token(text, 'number', *tokenArgs) - else: - return Token(text, 'identifier', *tokenArgs) - - def translate(self): - """ Translate the code by applying fixes to the tokens. Returns - the new code as a string. - """ - - # Collect fixers. Sort by name, so at least its consistent. - fixers = [] - for name in sorted(dir(self)): - if name.startswith('fix_'): - fixers.append(getattr(self, name)) - - # Apply fixers - new_tokens = [] - for i, token in enumerate(self.tokens): - for fixer in fixers: - new_token = fixer(token) - if isinstance(new_token, Token): - assert new_token.start == new_token.end - if new_token.start <= token.start: - new_tokens.append((i, new_token)) - else: - new_tokens.append((i+1, new_token)) - - # Insert new tokens - for i, new_token in reversed(new_tokens): - self._tokens.insert(i, new_token) - - return self.dumps() - - def dumps(self): - """ Return a string with the translated code. - """ - text = self._text - pos = len(self._text) - pieces = [] - for t in reversed(self.tokens): - pieces.append(text[t.end:pos]) - pieces.append(t.fix if t.fix is not None else t.text) - pos = t.start - pieces.append(text[:pos]) - return ''.join(reversed(pieces)) - - @classmethod - def translate_dir(cls, dirname, skip=()): - """ Classmethod to translate all .py files in the given - directory and its subdirectories. Skips files that match names - in skip (which can be full file names, absolute paths, and paths - relative to dirname). Any file that imports 'print_function' - from __future__ is cancelled. - """ - dirname = os.path.normpath(dirname) - skip = [os.path.normpath(p) for p in skip] - for root, dirs, files in os.walk(dirname): - for fname in files: - if fname.endswith('.py'): - filename = os.path.join(root, fname) - relpath = os.path.relpath(filename, dirname) - if fname in skip or relpath in skip or filename in skip: - print('%s skipped: %r' % (cls.__name__, relpath)) - continue - code = open(filename, 'rb').read().decode('utf-8') - try: - new_code = cls(code).translate() - except CancelTranslation: - print('%s cancelled: %r' % (cls.__name__, relpath)) - else: - with open(filename, 'wb') as f: - f.write(new_code.encode('utf-8')) - print('%s translated: %r' % (cls.__name__, relpath)) - - -class LegacyPythonTranslator(BaseTranslator): - """ A Translator to translate Python 3 to Python 2.7. - """ - - FUTURES = ('print_function', 'absolute_import', 'with_statement', - 'unicode_literals', 'division') - - def dumps(self): - return '# -*- coding: utf-8 -*-\n' + BaseTranslator.dumps(self) - - def fix_cancel(self, token): - """ Cancel translation if using `from __future__ import xxx` - """ - if token.type == 'keyword' and (token.text == 'from' and - token.next_token.text == '__future__'): - for future in self.FUTURES: - if any([t.text == future for t in token.line_tokens]): - # Assume this module is already Python 2.7 compatible - raise CancelTranslation() - - def fix_future(self, token): - """ Fix print_function, absolute_import, with_statement. - """ - - status = getattr(self, '_future_status', 0) - if status == 2: - return # Done - - if status == 0 and token.type == 'string': - self._future_status = 1 # docstring - elif token.type != 'comment': - self._future_status = 2 # done - i = max(0, token.find_backward('\n')) - t = Token(token.total_text, '', i, i) - t.fix = '\nfrom __future__ import %s\n' % (', '.join(self.FUTURES)) - return t - - def fix_newstyle(self, token): - """ Fix to always use new style classes. - """ - if token.type == 'keyword' and token.text == 'class': - nametoken = token.next_token - if nametoken.next_char != '(': - nametoken.fix = '%s(object)' % nametoken.text - - def fix_super(self, token): - """ Fix super() -> super(Cls, self) - """ - # First keep track of the current class - if token.type == 'keyword': - if token.text == 'class': - self._current_class = token.indentation, token.next_token.text - elif token.text == 'def': - indent, name = getattr(self, '_current_class', (0, '')) - if token.indentation <= indent: - self._current_class = 0, '' - - # Then check for super - if token.type == 'identifier' and token.text == 'super': - if token.prev_char != '.' and token.next_char == '(': - i = token.find_forward(')') - sub = token.total_text[token.end:i+1] - if re.sub(r"\s+", '', sub) == '()': - indent, name = getattr(self, '_current_class', (0, '')) - if name: - token.end = i + 1 - token.fix = 'super(%s, self)' % name - - # Note: we use "from __future__ import unicode_literals" - # def fix_unicode_literals(self, token): - # if token.type == 'string': - # if token.text.lstrip('r').startswith(('"', "'")): # i.e. no b/u - # token.fix = 'u' + token.text - - def fix_unicode(self, token): - if token.type == 'identifier': - if token.text == 'chr' and token.next_char == '(': - # Calling chr - token.fix = 'unichr' - elif token.text == 'str' and token.next_char == '(': - # Calling str - token.fix = 'unicode' - elif token.text == 'str' and (token.next_char == ')' and - token.prev_char == '(' and - token.line_tokens[0].text == 'class'): - token.fix = 'unicode' - elif token.text == 'isinstance' and token.next_char == '(': - # Check for usage of str in isinstance - end = token.find_forward(')') - t = token.next_token - while t.next_token and t.next_token.start < end: - t = t.next_token - if t.text == 'str': - t.fix = 'basestring' - - def fix_range(self, token): - if token.type == 'identifier' and token.text == 'range': - if token.next_char == '(' and token.prev_char != '.': - token.fix = 'xrange' - - def fix_encode(self, token): - if token.type == 'identifier' and token.text in('encode', 'decode'): - if token.next_char == '(' and token.prev_char == '.': - end = token.find_forward(')') - if not (token.next_token and token.next_token.start < end): - token.fix = token.text + '("utf-8")' - token.end = end + 1 - - def fix_getcwd(self, token): - """ Fix os.getcwd -> os.getcwdu - """ - if token.type == 'identifier' and token.text == 'getcwd': - if token.next_char == '(': - token.fix = 'getcwdu' - - def fix_imports(self, token): - """ import xx.yy -> import zz - """ - if token.type == 'keyword' and token.text == 'import': - tokens = token.line_tokens - - # For each import case ... - for name, replacement in self.IMPORT_MAPPING.items(): - parts = name.split('.') - # Walk over tokens to find start of match - for i in range(len(tokens)): - if (tokens[i].text == parts[0] and - len(tokens[i:]) >= len(parts)): - # Is it a complete match? - for j, part in enumerate(parts): - if tokens[i+j].text != part: - break - else: - # Match, marge tokens - tokens[i].end = tokens[i+len(parts)-1].end - tokens[i].fix = replacement - for j in range(1, len(parts)): - tokens[i+j].start = tokens[i].end - tokens[i+j].end = tokens[i].end - tokens[i+j].fix = '' - break # we have found the match - - def fix_imports2(self, token): - """ from xx.yy import zz -> from vv import zz - """ - if token.type == 'keyword' and token.text == 'import': - tokens = token.line_tokens - - # We use the fact that all imports keys consist of two names - if tokens[0].text == 'from' and len(tokens) == 5: - if tokens[3].text == 'import': - xxyy = tokens[1].text + '.' + tokens[2].text - name = tokens[4].text - if xxyy in self.IMPORT_MAPPING2: - for possible_module in self.IMPORT_MAPPING2[xxyy]: - if name in self.PY2MODULES[possible_module]: - tokens[1].fix = possible_module - tokens[1].end = tokens[2].end - tokens[2].start = tokens[2].end - break - - - # Map simple import paths to new import import paths - IMPORT_MAPPING = { - "reprlib": "repr", - "winreg": "_winreg", - "configparser": "ConfigParser", - "copyreg": "copy_reg", - "queue": "Queue", - "socketserver": "SocketServer", - "_markupbase": "markupbase", - "test.support": "test.test_support", - "dbm.bsd": "dbhash", - "dbm.ndbm": "dbm", - "dbm.dumb": "dumbdbm", - "dbm.gnu": "gdbm", - "html.parser": "HTMLParser", - "html.entities": "htmlentitydefs", - "http.client": "httplib", - "http.cookies": "Cookie", - "http.cookiejar": "cookielib", - "urllib.robotparser": "robotparser", - "xmlrpc.client": "xmlrpclib", - "builtins": "__builtin__", - } - - - # Map import paths to ... a set of possible import paths - IMPORT_MAPPING2 = { - 'urllib.request': ('urllib2', 'urllib'), - 'urllib.error': ('urllib2', 'urllib'), - 'urllib.parse': ('urllib2', 'urllib', 'urlparse'), - 'dbm.__init__': ('anydbm', 'whichdb'), - 'http.server': ('CGIHTTPServer', 'SimpleHTTPServer', 'BaseHTTPServer'), - 'xmlrpc.server': ('DocXMLRPCServer', 'SimpleXMLRPCServer'), - } - - # This defines what names are in specific Python 2 modules - PY2MODULES = { - 'urllib2' : ( - 'AbstractBasicAuthHandler', 'AbstractDigestAuthHandler', - 'AbstractHTTPHandler', 'BaseHandler', 'CacheFTPHandler', - 'FTPHandler', 'FileHandler', 'HTTPBasicAuthHandler', - 'HTTPCookieProcessor', 'HTTPDefaultErrorHandler', - 'HTTPDigestAuthHandler', 'HTTPError', 'HTTPErrorProcessor', - 'HTTPHandler', 'HTTPPasswordMgr', - 'HTTPPasswordMgrWithDefaultRealm', 'HTTPRedirectHandler', - 'HTTPSHandler', 'OpenerDirector', 'ProxyBasicAuthHandler', - 'ProxyDigestAuthHandler', 'ProxyHandler', 'Request', - 'StringIO', 'URLError', 'UnknownHandler', 'addinfourl', - 'build_opener', 'install_opener', 'parse_http_list', - 'parse_keqv_list', 'randombytes', 'request_host', 'urlopen'), - 'urllib' : ( - 'ContentTooShortError', 'FancyURLopener', 'URLopener', - 'basejoin', 'ftperrors', 'getproxies', - 'getproxies_environment', 'localhost', 'pathname2url', - 'quote', 'quote_plus', 'splitattr', 'splithost', - 'splitnport', 'splitpasswd', 'splitport', 'splitquery', - 'splittag', 'splittype', 'splituser', 'splitvalue', - 'thishost', 'unquote', 'unquote_plus', 'unwrap', - 'url2pathname', 'urlcleanup', 'urlencode', 'urlopen', - 'urlretrieve',), - 'urlparse' : ( - 'parse_qs', 'parse_qsl', 'urldefrag', 'urljoin', - 'urlparse', 'urlsplit', 'urlunparse', 'urlunsplit'), - 'dbm' : ( - 'ndbm', 'gnu', 'dumb'), - 'anydbm' : ( - 'error', 'open'), - 'whichdb' : ( - 'whichdb',), - 'BaseHTTPServer' : ( - 'BaseHTTPRequestHandler', 'HTTPServer'), - 'CGIHTTPServer' : ( - 'CGIHTTPRequestHandler',), - 'SimpleHTTPServer' : ( - 'SimpleHTTPRequestHandler',), - 'DocXMLRPCServer' : ( - 'DocCGIXMLRPCRequestHandler', 'DocXMLRPCRequestHandler', - 'DocXMLRPCServer', 'ServerHTMLDoc', 'XMLRPCDocGenerator'), - } - - -if __name__ == '__main__': - # Awesome for testing - - code = """ - """ - - t = LegacyPythonTranslator(code) - new_code = t.translate() - print(t.tokens) - print('---') - print(new_code) From 3b04c829f46e40706525298c40c8b5d33d3b4209 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 24 Jan 2025 12:44:10 +0100 Subject: [PATCH 2/8] fix typo --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56ac251e..f9066e84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: python-version: 3.12 - name: Set up Node uses: actions/setup-node@v4 - with: + with: node-version: '18' - name: Install dependencies run: | From 2a4513e3eee55445bd1329a9b1bdd29dbeddc7f2 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 24 Jan 2025 12:45:14 +0100 Subject: [PATCH 3/8] fix another typo --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9066e84..2d481d62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: run: | ruff format --check . - docs: + docs: name: Docs runs-on: ubuntu-latest strategy: From 1c014e208cc375885eba55265f0ca712ad589d14 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 24 Jan 2025 12:49:27 +0100 Subject: [PATCH 4/8] update ci --- .github/workflows/ci.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d481d62..737bdc49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,20 +60,16 @@ jobs: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: include: - - name: Linux py36 - os: ubuntu-latest - pyversion: '3.6' - - name: Linux py37 - os: ubuntu-latest - pyversion: '3.7' - name: Linux py38 os: ubuntu-latest pyversion: '3.8' - name: Linux py39 os: ubuntu-latest pyversion: '3.9' + # - name: Linux pypy3 os: ubuntu-latest pyversion: 'pypy3' From 2f7ae8714e6a404cd61726a3e1c88cbc3ace0a49 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 24 Jan 2025 12:55:22 +0100 Subject: [PATCH 5/8] drop more old stuff --- .github/workflows/ci.yml | 6 +- tasks/README.md | 33 ----------- tasks/__init__.py | 43 -------------- tasks/__main__.py | 14 ----- tasks/_config.py | 11 ---- tasks/clean.py | 33 ----------- tasks/copyright.py | 38 ------------ tasks/docs.py | 63 -------------------- tasks/help.py | 17 ------ tasks/pscript.py | 10 ---- tasks/test.py | 123 --------------------------------------- 11 files changed, 3 insertions(+), 388 deletions(-) delete mode 100644 tasks/README.md delete mode 100644 tasks/__init__.py delete mode 100644 tasks/__main__.py delete mode 100644 tasks/_config.py delete mode 100644 tasks/clean.py delete mode 100644 tasks/copyright.py delete mode 100644 tasks/docs.py delete mode 100644 tasks/help.py delete mode 100644 tasks/pscript.py delete mode 100644 tasks/test.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 737bdc49..a4834c55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,7 @@ jobs: # - name: Linux pypy3 os: ubuntu-latest - pyversion: 'pypy3' + pyversion: 'pypy3.9' - name: Windows py38 os: windows-latest pyversion: '3.8' @@ -89,7 +89,7 @@ jobs: - name: Install dependencies for unit tests run: | python -m pip install --upgrade pip - pip install invoke pytest pytest-cov + pip install pytest pytest-cov - name: Test with pytest run: | - invoke test --unit + pytest -v --cov pscript --cov-config=.coveragerc --cov-report=term --cov-report=html pscript diff --git a/tasks/README.md b/tasks/README.md deleted file mode 100644 index f371d9a2..00000000 --- a/tasks/README.md +++ /dev/null @@ -1,33 +0,0 @@ ------ -tasks ------ - -Tools for developers, such as testing, building docs/website, etc. - -Usage:: - - invoke task ... - invoke --help task - -This makes use of the invoke package to translate CLI commands to function -calls. This package is set up so that new tasks can be added simply by adding -a module that defines one or more tasks, this makes it easy to share tasks -between projects. - -Each project must implement its own _config.py, so that the tasks themselves -can be project-agnostic. - -Names that you can `from ._config import ...`: - -* NAME - the name of the project -* THIS_DIR - the path of the tasks directory -* ROOT_DIR - the root path of the repository -* DOC_DIR - the path to the docs -* DOC_BUILD_DIR - the path to where the docs are build -* ... - more may be added, depending on what tasks are present - - -License -------- - -Consider this code public domain unless stated otherwise. diff --git a/tasks/__init__.py b/tasks/__init__.py deleted file mode 100644 index a171e237..00000000 --- a/tasks/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Main file for invoke tasks. We auto-import all modules here, so that -one can simply add tasks by adding files. -""" - -import os - -from invoke import Collection, Task - -# Get root directory of the package -THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -ROOT_DIR = os.path.dirname(THIS_DIR) - -# Init collection; invoke picks this up as the main "list of tasks" -ns = Collection() - -# Automatically collect tasks from submodules -for fname in os.listdir(THIS_DIR): - # Does this look like a module that we want? - if fname.startswith('_') or not fname.endswith('.py'): - continue - modname = fname[:-3] - # Import it - m = __import__(modname, level=1, fromlist=[], globals=globals()) - # Collect all tasks and collections - collections, tasks = {}, {} - for name in dir(m): - ob = getattr(m, name) - if isinstance(ob, Task): - tasks[name] = ob - elif isinstance(ob, Collection): - collections[name] = ob - # Add collections - for name, ob in collections.items(): - ns.add_collection(ob, name) - # Add tasks that are not already in a collection - for name, ob in tasks.items(): - add_task = True - for c in collections.values(): - if ob in c.tasks.values(): - add_task = False - if add_task: - ns.add_task(ob, name) diff --git a/tasks/__main__.py b/tasks/__main__.py deleted file mode 100644 index 523ac421..00000000 --- a/tasks/__main__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -Make this module itself executable as an alias for invoke. -""" - -import sys -import subprocess - -cmd = ['invoke'] -if len(sys.argv) == 1: - cmd.append('help') -else: - cmd.extend(sys.argv[1:]) - -subprocess.check_call(cmd) diff --git a/tasks/_config.py b/tasks/_config.py deleted file mode 100644 index db6b5087..00000000 --- a/tasks/_config.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -Config and definitions specific to PScript. -""" - -import os.path as op - -from . import ROOT_DIR, THIS_DIR # noqa - -NAME = 'pscript' -DOC_DIR = op.join(ROOT_DIR, 'docs') -DOC_BUILD_DIR = op.join(DOC_DIR, '_build') diff --git a/tasks/clean.py b/tasks/clean.py deleted file mode 100644 index 26351302..00000000 --- a/tasks/clean.py +++ /dev/null @@ -1,33 +0,0 @@ -import os -import shutil -import fnmatch - -from invoke import task - -from ._config import ROOT_DIR, NAME - - -@task -def clean(ctx): - """ clear all .pyc modules and __pycache__ dirs - """ - count1, count2 = 0, 0 - - for root, dirnames, filenames in os.walk(ROOT_DIR): - for dirname in dirnames: - if dirname == '__pycache__': - shutil.rmtree(os.path.join(root, dirname)) - count1 += 1 - print('removed %i __pycache__ dirs' % count1) - - for root, dirnames, filenames in os.walk(ROOT_DIR): - for filename in fnmatch.filter(filenames, '*.pyc'): - os.remove(os.path.join(root, filename)) - count2 += 1 - print('removed %i .pyc files' % count2) - - for dir in ['dist', 'build', NAME+'.egg-info', 'htmlcov']: - dirname = os.path.join(ROOT_DIR, dir) - if os.path.isdir(dirname): - shutil.rmtree(dirname) - print('Removed directory %r' % dir) diff --git a/tasks/copyright.py b/tasks/copyright.py deleted file mode 100644 index f7800924..00000000 --- a/tasks/copyright.py +++ /dev/null @@ -1,38 +0,0 @@ -import os - -from ._config import ROOT_DIR - -from invoke import task - - -@task -def copyright(ctx): - """ list usage of copyright notices - - The use of copyright notices should be limited to files that are likely - to be used in other projects, or to make appropriate attributions for code - taken from other projects. Other than that, git geeps track of what person - wrote what. - """ - - # Processing the whole root directory - for dirpath, dirnames, filenames in os.walk(ROOT_DIR): - # Check if we should skip this directory - reldirpath = os.path.relpath(dirpath, ROOT_DIR) - if reldirpath[0] in '._' or reldirpath.endswith('__pycache__'): - continue - if os.path.split(reldirpath)[0] in ('build', 'dist'): - continue - # Process files - for fname in filenames: - if not fname.endswith('.py'): - continue - # Open and check - filename = os.path.join(dirpath, fname) - text = open(filename, 'rt', encoding='utf-8').read() - if 'copyright' in text[:200].lower(): - print( - 'Copyright in %s%s%s' % (reldirpath, os.path.sep, fname)) - for i, line in enumerate(text[:200].splitlines()): - if 'copyright' in line.lower(): - print(' line %i: %s' % (i+1, line)) diff --git a/tasks/docs.py b/tasks/docs.py deleted file mode 100644 index a2304e8a..00000000 --- a/tasks/docs.py +++ /dev/null @@ -1,63 +0,0 @@ -import os -import sys -import os.path as op -import shutil - -from invoke import task -from ._config import DOC_DIR, DOC_BUILD_DIR - - -@task(help=dict(clean='clear the doc output; start fresh', - build='build html docs', - show='show the docs in the browser.')) -def docs(ctx, clean=False, build=False, show=False, **kwargs): - """ make API documentation - """ - # Prepare - - if not (clean or build or show): - sys.exit('Task "docs" must be called with --clean, --build or --show') - - if clean: - sphinx_clean(DOC_BUILD_DIR) - - if build: - sphinx_build(DOC_DIR, DOC_BUILD_DIR) - - if show: - sphinx_show(os.path.join(DOC_BUILD_DIR, 'html')) - - -def sphinx_clean(build_dir): - if op.isdir(build_dir): - shutil.rmtree(build_dir) - os.mkdir(build_dir) - os.mkdir(os.path.join(build_dir, 'html')) - print('Cleared build directory.') - - -def sphinx_build(src_dir, build_dir): - import sphinx - cmd = [ - '-b', 'html', - '-d', op.join(build_dir, 'doctrees'), - src_dir, # Source - op.join(build_dir, 'html'), # Dest - ] - - if sphinx.version_info > (1, 7): - import sphinx.cmd.build - ret = sphinx.cmd.build.build_main(cmd) - else: - ret = sphinx.build_main(['sphinx-build'] + cmd) - if ret != 0: - raise RuntimeError('Sphinx error: %s' % ret) - print("Build finished. The HTML pages are in %s/html." % build_dir) - - -def sphinx_show(html_dir): - index_html = op.join(html_dir, 'index.html') - if not op.isfile(index_html): - sys.exit('Cannot show pages, build the html first.') - import webbrowser - webbrowser.open_new_tab(index_html) diff --git a/tasks/help.py b/tasks/help.py deleted file mode 100644 index fdc5fa9b..00000000 --- a/tasks/help.py +++ /dev/null @@ -1,17 +0,0 @@ -import subprocess - -from invoke import task - -from ._config import NAME - - -@task -def help(ctx): - """Get info on usage. - """ - - print('Developer tools for project %s\n' % NAME.capitalize()) - print(' invoke [arg] to run a task') - print(' invoke --help to get info on a task') - print() - subprocess.call('invoke --list') diff --git a/tasks/pscript.py b/tasks/pscript.py deleted file mode 100644 index fc7833fe..00000000 --- a/tasks/pscript.py +++ /dev/null @@ -1,10 +0,0 @@ -from invoke import task - -# todo: also print meta info like globals etc. - -@task(help=dict(code='the Python code to transpile')) -def py2js(ctx, code): - """transpile given Python code to JavaScript - """ - from pscript import py2js - print(py2js(code)) diff --git a/tasks/test.py b/tasks/test.py deleted file mode 100644 index 6942baf6..00000000 --- a/tasks/test.py +++ /dev/null @@ -1,123 +0,0 @@ -import os -import sys - -from invoke import task - -from ._config import ROOT_DIR, NAME - - -@task -def lint(ctx): - """ alias for "invoke test --style" - """ - test_style() - - -@task(optional=['unit', 'style'], - help=dict(unit='run unit tests (pytest) on given subdir (default ".")', - style='run style tests (flake8) on given subdir (default ".")', - cover='show test coverage')) -def test(ctx, unit='', style='', cover=False): - """ run tests (unit, style) - """ - - if not (unit or style or cover): - sys.exit('Test task needs --unit, --style or --cover') - if unit: - test_unit('.' if not isinstance(unit, str) else unit) - if style: - test_style('.' if not isinstance(style, str) else style) - if cover: - show_coverage_html() - - -def test_unit(rel_path='.'): - # Ensure we have pytest - try: - import pytest # noqa - except ImportError: - sys.exit('Cannot do unit tests, pytest not installed') - # Get path to test - rel_path = 'pscript/' + rel_path - test_path = os.path.join(ROOT_DIR, rel_path) - # Import from installed, or from ROOT_DIR - if os.getenv('TEST_INSTALL', '').lower() in ('1', 'yes', 'true'): - if ROOT_DIR in sys.path: - sys.path.remove(ROOT_DIR) - os.chdir(os.path.expanduser('~')) - m = __import__(NAME) - assert ROOT_DIR not in os.path.abspath(m.__path__[0]) - else: - os.chdir(ROOT_DIR) - m = __import__(NAME) - assert ROOT_DIR in os.path.abspath(m.__path__[0]) - # Start tests - _enable_faulthandler() - try: - res = pytest.main(['--cov', NAME, '--cov-config=.coveragerc', - '--cov-report=term', '--cov-report=html', test_path]) - sys.exit(res) - finally: - m = __import__(NAME) - print('Unit tests were performed on', str(m)) - - -def show_coverage_term(): - from coverage import coverage - cov = coverage(auto_data=False, branch=True, data_suffix=None, - source=[NAME]) # should match testing/_coverage.py - cov.load() - cov.report() - - -def show_coverage_html(): - import webbrowser - from coverage import coverage - - print('Generating HTML...') - os.chdir(ROOT_DIR) - cov = coverage(auto_data=False, branch=True, data_suffix=None, - source=[NAME]) # should match testing/_coverage.py - cov.load() - cov.html_report() - print('Done, launching browser.') - fname = os.path.join(os.getcwd(), 'htmlcov', 'index.html') - if not os.path.isfile(fname): - raise IOError('Generated file not found: %s' % fname) - webbrowser.open_new_tab(fname) - - -def test_style(rel_path='.'): - # Ensure we have flake8 - try: - import flake8 # noqa - from flake8.main.application import Application - except ImportError as err: - sys.exit('Cannot do style test: ' + str(err)) - # Prepare - os.chdir(ROOT_DIR) - sys.argv[1:] = ['pscript/' + rel_path] - # Do test - print('Running flake8 tests ...') - app = Application() - app.run([]) - # Report - nerrors = app.result_count - if nerrors: - print('Arg! Found %i style errors.' % nerrors) - else: - print('Hooray, no style errors found!') - # Exit (will exit(1) if errors) - app.exit() - - -def _enable_faulthandler(): - """ Enable faulthandler (if we can), so that we get tracebacks - on segfaults. - """ - try: - import faulthandler - faulthandler.enable() - print('Faulthandler enabled') - except Exception: - print('Could not enable faulthandler') From 4931937737845c7e5faab93c21187e6b689fc53c Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 24 Jan 2025 12:57:54 +0100 Subject: [PATCH 6/8] more modern --- .github/workflows/ci.yml | 5 ++++- {pscript/tests => tests}/python_sample.bz2 | Bin {pscript/tests => tests}/python_sample.py | 0 {pscript/tests => tests}/python_sample2.bz2 | Bin {pscript/tests => tests}/python_sample2.py | 0 {pscript/tests => tests}/python_sample3.bz2 | Bin {pscript/tests => tests}/python_sample3.py | 0 {pscript/tests => tests}/test_commonast.py | 0 {pscript/tests => tests}/test_functions.py | 0 {pscript/tests => tests}/test_modules.py | 0 {pscript/tests => tests}/test_parser0.py | 0 {pscript/tests => tests}/test_parser1.py | 0 {pscript/tests => tests}/test_parser2.py | 0 {pscript/tests => tests}/test_parser3.py | 0 {pscript/tests => tests}/test_stdlib.py | 0 {pscript/tests => tests}/test_stubs.py | 0 16 files changed, 4 insertions(+), 1 deletion(-) rename {pscript/tests => tests}/python_sample.bz2 (100%) rename {pscript/tests => tests}/python_sample.py (100%) rename {pscript/tests => tests}/python_sample2.bz2 (100%) rename {pscript/tests => tests}/python_sample2.py (100%) rename {pscript/tests => tests}/python_sample3.bz2 (100%) rename {pscript/tests => tests}/python_sample3.py (100%) rename {pscript/tests => tests}/test_commonast.py (100%) rename {pscript/tests => tests}/test_functions.py (100%) rename {pscript/tests => tests}/test_modules.py (100%) rename {pscript/tests => tests}/test_parser0.py (100%) rename {pscript/tests => tests}/test_parser1.py (100%) rename {pscript/tests => tests}/test_parser2.py (100%) rename {pscript/tests => tests}/test_parser3.py (100%) rename {pscript/tests => tests}/test_stdlib.py (100%) rename {pscript/tests => tests}/test_stubs.py (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4834c55..0e665434 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,9 @@ jobs: run: | python -m pip install --upgrade pip pip install pytest pytest-cov + pip install . + rm -rf ./pscript ./build ./egg-info - name: Test with pytest run: | - pytest -v --cov pscript --cov-config=.coveragerc --cov-report=term --cov-report=html pscript + python -c "import sys; print(sys.version, '\n', sys.prefix)"; + pytest -v --cov pscript --cov-config=.coveragerc --cov-report=term --cov-report=html tests diff --git a/pscript/tests/python_sample.bz2 b/tests/python_sample.bz2 similarity index 100% rename from pscript/tests/python_sample.bz2 rename to tests/python_sample.bz2 diff --git a/pscript/tests/python_sample.py b/tests/python_sample.py similarity index 100% rename from pscript/tests/python_sample.py rename to tests/python_sample.py diff --git a/pscript/tests/python_sample2.bz2 b/tests/python_sample2.bz2 similarity index 100% rename from pscript/tests/python_sample2.bz2 rename to tests/python_sample2.bz2 diff --git a/pscript/tests/python_sample2.py b/tests/python_sample2.py similarity index 100% rename from pscript/tests/python_sample2.py rename to tests/python_sample2.py diff --git a/pscript/tests/python_sample3.bz2 b/tests/python_sample3.bz2 similarity index 100% rename from pscript/tests/python_sample3.bz2 rename to tests/python_sample3.bz2 diff --git a/pscript/tests/python_sample3.py b/tests/python_sample3.py similarity index 100% rename from pscript/tests/python_sample3.py rename to tests/python_sample3.py diff --git a/pscript/tests/test_commonast.py b/tests/test_commonast.py similarity index 100% rename from pscript/tests/test_commonast.py rename to tests/test_commonast.py diff --git a/pscript/tests/test_functions.py b/tests/test_functions.py similarity index 100% rename from pscript/tests/test_functions.py rename to tests/test_functions.py diff --git a/pscript/tests/test_modules.py b/tests/test_modules.py similarity index 100% rename from pscript/tests/test_modules.py rename to tests/test_modules.py diff --git a/pscript/tests/test_parser0.py b/tests/test_parser0.py similarity index 100% rename from pscript/tests/test_parser0.py rename to tests/test_parser0.py diff --git a/pscript/tests/test_parser1.py b/tests/test_parser1.py similarity index 100% rename from pscript/tests/test_parser1.py rename to tests/test_parser1.py diff --git a/pscript/tests/test_parser2.py b/tests/test_parser2.py similarity index 100% rename from pscript/tests/test_parser2.py rename to tests/test_parser2.py diff --git a/pscript/tests/test_parser3.py b/tests/test_parser3.py similarity index 100% rename from pscript/tests/test_parser3.py rename to tests/test_parser3.py diff --git a/pscript/tests/test_stdlib.py b/tests/test_stdlib.py similarity index 100% rename from pscript/tests/test_stdlib.py rename to tests/test_stdlib.py diff --git a/pscript/tests/test_stubs.py b/tests/test_stubs.py similarity index 100% rename from pscript/tests/test_stubs.py rename to tests/test_stubs.py From 8b8492b0d6f535ba2b7226a861929795b01faf8f Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 24 Jan 2025 13:05:36 +0100 Subject: [PATCH 7/8] tweaks --- pscript/__init__.py | 4 ++-- tests/test_commonast.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pscript/__init__.py b/pscript/__init__.py index 86ca574c..1782d944 100644 --- a/pscript/__init__.py +++ b/pscript/__init__.py @@ -246,6 +246,8 @@ import sys import logging +logger = logging.getLogger(__name__) + # NOTE: The code for the parser is quite long, especially if you want # to document it well. Therefore it is split in multiple modules, which @@ -270,6 +272,4 @@ from .stubs import RawJS, JSConstant, window, undefined -logger = logging.getLogger(__name__) - del logging, sys diff --git a/tests/test_commonast.py b/tests/test_commonast.py index f8bca9dd..3d0352a1 100644 --- a/tests/test_commonast.py +++ b/tests/test_commonast.py @@ -10,9 +10,9 @@ from pscript.testing import run_tests_if_main, raises, skipif -sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) -import commonast -#from pscript import commonast +# sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) +# import commonast +from pscript import commonast dirname = os.path.dirname(__file__) @@ -118,7 +118,7 @@ class MyNode(Node): raises(AssertionError, MyNode, 'a', 'Add', stubnode, stubnodes, stubnodes) def test_json_conversion(): - from commonast import Node, Assign, Name, BinOp, Bytes, Num + from pscript.commonast import Node, Assign, Name, BinOp, Bytes, Num # Test json conversion roota = Assign([Name('foo')], BinOp('Add', Name('a'), Num(3))) @@ -304,7 +304,7 @@ def bar(a:[], b:(1,2), *c:'xx', **d:'yy') -> 'returns': def test_call_some_more(): - from commonast import Name, Num, Starred, Keyword + from pscript.commonast import Name, Num, Starred, Keyword code = "foo(1, a, *b, c=3, **d)" node = commonast.parse(code).body_nodes[0].value_node # Call is in an Expr @@ -322,7 +322,7 @@ def test_call_some_more(): @skipif(sys.version_info < (3,5), reason='Need Python 3.5+') def test_call_even_some_more(): - from commonast import Name, Num, Starred, Keyword + from pscript.commonast import Name, Num, Starred, Keyword code = "foo(a, *b, c, *d, **e, **f)" node = commonast.parse(code).body_nodes[0].value_node From b3f215dfed6c6b487b1c22233cdd264c17411a20 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 24 Jan 2025 13:12:13 +0100 Subject: [PATCH 8/8] fix --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e665434..6021afa9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,7 @@ jobs: with: python-version: ${{ matrix.pyversion }} - name: Install dependencies for unit tests + shell: bash run: | python -m pip install --upgrade pip pip install pytest pytest-cov