diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e74ea1d..8f17d82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: with: python-version: '3.11' - name: Restore cache - uses: actions/cache@v3.2.6 + uses: actions/cache@v3.3.1 with: path: '~/.cache/pip' key: ${{ runner.os }}-pip-${{ hashFiles('.vintrc.yml') }} @@ -33,29 +33,29 @@ jobs: strategy: matrix: vim-version: [7.4.1689, 8.2.4049, 9.0.0304] - python-version: ['2.7.18', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11', 'ubuntu'] + python-version: ['3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11', 'ubuntu'] env: LOG: python-${{ matrix.python-version }}-bench.log steps: - uses: actions/checkout@v3 - name: Set up python - if: ${{ !startswith(matrix.python-version, 'ubuntu') }} + if: ${{ matrix.python-version != 'ubuntu' }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install latest pip - if: ${{ !(startsWith(matrix.python-version, '2.7') || startsWith(matrix.python-version, '3.5')) }} + if: ${{ !startsWith(matrix.python-version, '3.5') }} run: | pip install --upgrade pip - name: Install pip < 21.0 - if: ${{ (startsWith(matrix.python-version, '2.7') || startsWith(matrix.python-version, '3.5')) }} + if: ${{ startsWith(matrix.python-version, '3.5') }} run: | pip install --upgrade 'pip < 21.0' - name: Restore pip cache - uses: actions/cache@v3.2.6 + uses: actions/cache@v3.3.1 with: path: '~/.cache/pip' key: ${{ matrix.python-version }}-pip-${{ hashFiles('**/test/requirements.txt') }} @@ -73,7 +73,7 @@ jobs: fetch-depth: 1 - name: Install neovim (current stable) - if: ${{ !(startsWith(matrix.python-version, '2.7') || startsWith(matrix.python-version, '3.5') || startsWith(matrix.python-version, '3.6')) }} + if: ${{ !(startsWith(matrix.python-version, '3.5') || startsWith(matrix.python-version, '3.6')) }} uses: rhysd/action-setup-vim@v1 id: neovim-stable with: @@ -95,28 +95,29 @@ jobs: # > from collections import Callable run: pytest -v -W ignore::DeprecationWarning - - name: Test plugin on vim with Python 3 - if: ${{ startswith(matrix.python-version, 'ubuntu') }} + - name: Test plugin on Ubuntu vim + if: ${{ matrix.python-version == 'ubuntu' }} run: | sudo apt-get -qq update sudo apt-get -qqy install vim vim --startuptime vim_bench.log -ENsu test/vimrc -c '+Vader! test/vader/**/*.vader' > /dev/null .github/scripts/collect_start_times vim vim-${{ matrix.vim-version }}-$LOG - - name: Test plugin on vim with Python 2 - if: ${{ startsWith(matrix.python-version, '2.7') }} + - name: Test plugin on vim ${{ matrix.vim-version }} + if: ${{ (startsWith(matrix.python-version, '3.5') || startsWith(matrix.python-version, '3.6')) }} run: | git clone --branch v${{ matrix.vim-version }} https://github.com/vim/vim.git vim-src pushd vim-src - ./configure --prefix=/usr/local --with-features=normal \ - --enable-pythoninterp=dynamic \ - --with-python-config-dir=$(find $Python_ROOT_DIR -iname Makefile | xargs dirname) + git grep -l '# undef _POSIX_THREADS' | xargs -I% sed -i '/# undef _POSIX_THREADS/d' % + CFLAGS=-D_POSIX_THREADS ./configure --prefix=/usr/local --with-features=normal \ + --enable-python3interp=dynamic \ + --with-python3-config-dir=$(find $Python_ROOT_DIR -iname Makefile | xargs dirname) make && sudo make install && popd - /usr/local/bin/vim --startuptime vim_bench.log -ENsu test/vimrc -c '+Vader! test/vader/pyx/*' > /dev/null + /usr/local/bin/vim --startuptime vim_bench.log -ENsu test/vimrc -c '+Vader! test/vader/*' > /dev/null .github/scripts/collect_start_times vim vim-${{ matrix.vim-version }}-$LOG - name: Test plugin on current stable neovim - if: ${{ !(startsWith(matrix.python-version, '2.7') || startsWith(matrix.python-version, '3.5') || startsWith(matrix.python-version, '3.6')) }} + if: ${{ !(startsWith(matrix.python-version, '3.5') || startsWith(matrix.python-version, '3.6')) }} run: | "${{ steps.neovim-stable.outputs.executable }}" --startuptime nvim_bench.log -ENsu test/vimrc -c '+Vader! test/vader/**/*.vader' > /dev/null @@ -126,7 +127,6 @@ jobs: "${{ steps.neovim.outputs.executable }}" --startuptime nvim_bench.log -ENsu test/vimrc -c '+Vader! test/vader/**/*.vader' > /dev/null - name: Check neovim startup times - if: ${{ !startsWith(matrix.python-version, '2.7') }} run: | .github/scripts/collect_start_times nvim neovim-${{ matrix.nvim-version }}-$LOG diff --git a/.gitignore b/.gitignore index caaeddc..37d71b9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .pytest_cache/ test/src/ vader.vim/ +!test/src/.gitkeep diff --git a/README.rst b/README.rst index 11ed17b..6cc18e1 100644 --- a/README.rst +++ b/README.rst @@ -37,7 +37,7 @@ copyright line will contain your OS user and host names. Features ======== -* Python bindings compatible with python 2.7.x or 3.4+, depending on your +* Python bindings compatible with python 3.7+, depending on your platform and (neo)vim version, of course :NOTE: Python 3.10 requires `neovim 0.6.0`_ or newer. @@ -146,23 +146,20 @@ Options Requirements ============ -* Vim compiled with any one of the *+python[3]* or *+python[3]/dyn* options. - See if you're supported by entering ``vim --version | grep +python`` at your +* Vim compiled with any one of the *+python3* or *+python3/dyn* options. + See if you're supported by entering ``vim --version | grep +python3`` at your terminal, or start ``vim`` and enter the ``:version`` command * Neovim with the `pynvim`_ module in your ``$PYTHONPATH``. Start ``nvim`` and enter ``:help provider-python`` for more information .. _pynvim: https://github.com/neovim/pynvim -.. _requests: https://pypi.org/project/requests Installation ============ -If `requests`_ and (optionally) `pynvim`_ are not already in your ``$PYTHONPATH``, -install them:: +If `pynvim`_ is not already in your ``$PYTHONPATH``, install it:: - pip install --user -U requests pip install --user -U pynvim diff --git a/autoload/cpywrite.vim b/autoload/cpywrite.vim index bd2c78f..d6a98b1 100644 --- a/autoload/cpywrite.vim +++ b/autoload/cpywrite.vim @@ -9,7 +9,7 @@ if get(g:, 'autoloaded_cpywrite') | finish | endif let g:autoloaded_cpywrite = 1 -if has('python3') || has('python') +if has('python3') if empty(get(s:, 'cpywrite_python_cmd', '')) let s:cpywrite_python_cmd = (has('python3') ? 'py3' : 'py') . 'file' endif diff --git a/autoload/cpywrite/error.vim b/autoload/cpywrite/error.vim index 0a88f42..7c4e22f 100644 --- a/autoload/cpywrite/error.vim +++ b/autoload/cpywrite/error.vim @@ -17,8 +17,7 @@ func! cpywrite#error#NoPython() abort else echohl WarningMsg | echom - \ 'Sorry: vim-cpywrite requires one of the following features: - \ +python, +python3, +python/dyn, or +python3/dyn. + \ 'Sorry: vim-cpywrite requires +python3 or +python3/dyn. \ Enter ":help python" for more information.' \ | echohl None endif diff --git a/doc/cpywrite.txt b/doc/cpywrite.txt index 91f0ccb..ec7fdb3 100644 --- a/doc/cpywrite.txt +++ b/doc/cpywrite.txt @@ -40,8 +40,7 @@ your OS user and host names. ============================================================================= REQUIREMENTS *cpywrite-requirements* -* Vim compiled with any one of the `+python`, `+python3`, `+python/dyn` or - `+python3/dyn` options +* Vim compiled with the |+python3| or |+python3/dyn| option * Neovim with the `pynvim` module in your $PYTHONPATH: @@ -49,10 +48,6 @@ REQUIREMENTS *cpywrite-requirements* Enter |:help| |provider-python| in your command prompt for more information. -* The `requests` module in your $PYTHONPATH: - - https://pypi.org/project/requests - ============================================================================= ATTRIBUTES *cpywrite-attributes* diff --git a/plugin/cpywrite/main.py b/plugin/cpywrite/main.py index 5c1cc9e..52409c8 100644 --- a/plugin/cpywrite/main.py +++ b/plugin/cpywrite/main.py @@ -8,7 +8,7 @@ import os import vim from re import match, sub -for path in vim.eval('globpath(&rtp,"rplugin/pythonx",1)').split('\n'): +for path in vim.eval('globpath(&rtp,"rplugin/python3",1)').split('\n'): sys.path.append(path) from cpywrite.generator import Generator, _get_source_author diff --git a/plugin/cpywrite/tabs.py b/plugin/cpywrite/tabs.py index 04acda4..2840b08 100644 --- a/plugin/cpywrite/tabs.py +++ b/plugin/cpywrite/tabs.py @@ -6,7 +6,7 @@ import sys import vim from re import match, IGNORECASE -for path in vim.eval('globpath(&rtp, "rplugin/pythonx", 1)').split('\n'): +for path in vim.eval('globpath(&rtp, "rplugin/python3", 1)').split('\n'): sys.path.append(path) from cpywrite import licenses diff --git a/rplugin/pythonx/cpywrite/__init__.py b/rplugin/python3/cpywrite/__init__.py similarity index 100% rename from rplugin/pythonx/cpywrite/__init__.py rename to rplugin/python3/cpywrite/__init__.py diff --git a/rplugin/pythonx/cpywrite/__main__.py b/rplugin/python3/cpywrite/__main__.py similarity index 87% rename from rplugin/pythonx/cpywrite/__main__.py rename to rplugin/python3/cpywrite/__main__.py index abe8034..2757111 100644 --- a/rplugin/pythonx/cpywrite/__main__.py +++ b/rplugin/python3/cpywrite/__main__.py @@ -3,12 +3,11 @@ """ Module entry point, when invoked at the command line """ -from __future__ import print_function import sys import tempfile from os import path from argparse import ArgumentParser, ArgumentError -from .generator import Generator, extensions +from cpywrite.generator import Generator, extensions def main(): """Prepend a license header to a new or existing source file""" @@ -56,24 +55,24 @@ def main(): args.no_anon) if license_text: if not path.exists(generator.out_file): - with open(generator.out_file, 'w') as src: + with open(generator.out_file, 'w', encoding='utf-8') as src: src.truncate(8) print("Created new %s file: %s" % (generator.lang, generator.out_file)) else: - with open(generator.out_file + '.bak', 'w') as bak: - with open(generator.out_file, 'rt') as source: + with open(generator.out_file + '.bak', 'w', encoding='utf-8') as bak: + with open(generator.out_file, 'rt', encoding='utf-8') as source: source_file = source.read() bak.write(source_file) _, tmp_source = tempfile.mkstemp(text=True) - with open(tmp_source, 'w') as tmp: + with open(tmp_source, 'w', encoding='utf-8') as tmp: tmp.write("%s%s" % (license_text, source_file)) - with open(tmp_source, 'rt') as new_content: - with open(generator.out_file, 'w') as source: + with open(tmp_source, 'rt', encoding='utf-8') as new_content: + with open(generator.out_file, 'w', encoding='utf-8') as source: source.write(new_content.read()) else: diff --git a/rplugin/pythonx/cpywrite/generator.py b/rplugin/python3/cpywrite/generator.py similarity index 99% rename from rplugin/pythonx/cpywrite/generator.py rename to rplugin/python3/cpywrite/generator.py index b8f58b8..9052afc 100644 --- a/rplugin/pythonx/cpywrite/generator.py +++ b/rplugin/python3/cpywrite/generator.py @@ -3,7 +3,6 @@ """ Module providing a file writer """ -from __future__ import print_function, unicode_literals import re import sys from datetime import datetime @@ -17,7 +16,7 @@ __all__ = ['Generator', 'extensions'] -class Generator(object): # pylint: disable=R0205 +class Generator(): """A source file generator""" def __init__(self, filename='new.py', vim_filetype='python', rights='Apache-2.0'): self.set_file_props(filename, vim_filetype, rights) diff --git a/rplugin/pythonx/cpywrite/spdx/__init__.py b/rplugin/python3/cpywrite/spdx/__init__.py similarity index 100% rename from rplugin/pythonx/cpywrite/spdx/__init__.py rename to rplugin/python3/cpywrite/spdx/__init__.py diff --git a/rplugin/pythonx/cpywrite/spdx/license.py b/rplugin/python3/cpywrite/spdx/license.py similarity index 91% rename from rplugin/pythonx/cpywrite/spdx/license.py rename to rplugin/python3/cpywrite/spdx/license.py index 2d50f16..bf00b91 100644 --- a/rplugin/pythonx/cpywrite/spdx/license.py +++ b/rplugin/python3/cpywrite/spdx/license.py @@ -3,25 +3,21 @@ """ Utilities for fetching and printing open source license information """ -from __future__ import print_function, unicode_literals import os import tempfile +import http.client as client import xml.etree.ElementTree as parser from itertools import dropwhile from re import search, sub from sys import stderr from textwrap import TextWrapper -import requests - -try: - from urllib.parse import quote -except ImportError: - from urllib import quote +from urllib import request, error as urllib_error +from urllib.parse import quote __all__ = ['License', 'licenses'] -class License(object): # pylint: disable=R0205 +class License(): """Encapsulates the details of a license on the SPDX index""" def __init__(self, code): self.spdx_code = None @@ -30,9 +26,9 @@ def __init__(self, code): try: self.spdx_code = _SPDX_IDS[_SPDX_IDS.index(code)] - except ValueError: + except ValueError as error: raise ValueError("No such license '%s' on the SPDX index!" - % (code)) + % (code)) from error @property def header(self): @@ -40,10 +36,6 @@ def header(self): if not self.spdx_code: return '' - # https://bugs.python.org/issue11033 - # ElementTree still has this problem in python 2.7.13 and - # probably later versions, too - strict_ascii = lambda c: ord(c) > 0 and ord(c) < 128 xml_resource = 'https://raw.githubusercontent.com/' \ 'spdx/license-list-XML/master/src/%s.xml' resource = quote(xml_resource % self.spdx_code, safe='/:') @@ -54,13 +46,13 @@ def header(self): if cache: try: - with open(cache, 'r') as tmp: + with open(cache, 'r', encoding='utf-8') as tmp: license_data = parser.fromstring(tmp.read()) except (parser.ParseError, IOError): pass if license_data is None: - license_data = _fetch_license(resource, self.spdx_code, strict_ascii) + license_data = _fetch_license(resource, self.spdx_code) if license_data is None: return '' @@ -82,21 +74,18 @@ def header(self): if child.attrib.get('name') == 'copyright': content = child.text if self.spdx_code.startswith('GFDL') and \ - child.tail and \ - any(filter(strict_ascii, child.tail)): + child.tail: content += child.tail.strip() + ' ' elif child.attrib.get('name') == 'version': content = child.text - if child.tail and \ - any(filter(strict_ascii, child.tail)): + if child.tail: content += child.tail or '' elif self.spdx_code.startswith('GFDL') and \ (child.attrib.get('name') == 'invariantSections' or \ child.attrib.get('name') == 'frontCoverTexts' or \ child.attrib.get('name') == 'backCoverTexts'): content = child.text - if child.tail and \ - any(filter(strict_ascii, child.tail)): + if child.tail: content += child.tail.strip() + ' ' elif child.tag == opt_tag and \ child.attrib.get('spacing') == 'after': @@ -185,7 +174,7 @@ def license_text(self): if cache: try: - with open(cache, 'r') as tmp: + with open(cache, 'r', encoding='utf-8') as tmp: license_text = tmp.read() except IOError: pass @@ -278,27 +267,23 @@ def _find_cached_license(short_name, ext='.xml'): return None -def _fetch_license(resource, spdx_id, validator=None): +def _fetch_license(resource, spdx_id): license_data = None - cached_content = None - ext = '.xml' if validator else '.txt' + _, ext = os.path.splitext(resource) try: - response = requests.get(resource) + response = request.urlopen(request.Request(resource)) - if response.status_code == 200: - if validator: + if response.status == 200: + response_text = response.read().decode('utf8') + if ext.lower() == '.xml': try: - safe_xml = \ - ''.join([ch for ch in response.text if validator(ch)]) - license_data = parser.fromstring(safe_xml) - cached_content = safe_xml - except (parser.ParseError, TypeError): + license_data = parser.fromstring(response_text) + except (client.IncompleteRead, parser.ParseError, TypeError): print("Got invalid licence data from %s." % (resource), file=stderr) else: - license_data = response.text - cached_content = license_data + license_data = response_text try: _, temp_file = \ @@ -307,8 +292,8 @@ def _fetch_license(resource, spdx_id, validator=None): text=True) if temp_file: try: - with open(temp_file, 'w') as tmp: - tmp.write(cached_content) + with open(temp_file, 'w', encoding='utf-8') as tmp: + tmp.write(response_text) except IOError: pass @@ -319,8 +304,8 @@ def _fetch_license(resource, spdx_id, validator=None): % (response.status_code, resource), file=stderr) - except (requests.exceptions.MissingSchema, - requests.exceptions.ConnectionError): + except (urllib_error.HTTPError, + urllib_error.URLError): print("Error requesting %s." % (resource), file=stderr) return license_data diff --git a/rplugin/pythonx/test/test_generator.py b/rplugin/python3/test/test_generator.py similarity index 100% rename from rplugin/pythonx/test/test_generator.py rename to rplugin/python3/test/test_generator.py diff --git a/test/conftest.py b/test/conftest.py index 0b897b5..aceffbd 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -4,4 +4,4 @@ import sys BASE_DIR = os.path.dirname(os.path.dirname(__file__)) -sys.path.insert(0, os.path.join(BASE_DIR, 'rplugin/pythonx')) +sys.path.insert(0, os.path.join(BASE_DIR, 'rplugin/python3')) diff --git a/test/requirements.txt b/test/requirements.txt index 0a19f3d..c928429 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -1,3 +1,2 @@ -requests pynvim pytest diff --git a/test/vader/py3/test_copyright_template_regexes.vader b/test/vader/py3/test_copyright_template_regexes.vader deleted file mode 100644 index 5a3f783..0000000 --- a/test/vader/py3/test_copyright_template_regexes.vader +++ /dev/null @@ -1,29 +0,0 @@ -After(Clear buffers); - %bd! - -Execute(Test that plugin was loaded); - call vader#log('===== Pre-test environment check =====') - Assert exists('g:loaded_cpywrite') - -Execute(Turn on verbatim output); - call vader#log('===== Switching to verbatim mode =====') - let g:cpywrite#verbatim_mode=1 - -Execute(Prepend the GD license verbatim to a Julia script); - new! verbatim.jl - b verbatim.jl - CPYwrite GD - -Then(Julia script preserves original third-party copyrights); - AssertEqual '# verbatim.jl', getline(2) - AssertEqual 1, len(getline(3)), 'Allow no trailing spaces' - AssertEqual '# Credits and license terms', getline(6) - AssertEqual "# \t\xe2\x80\xa2\tPortions copyright 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004 by Boutell.Com, Inc.", getline(11) - AssertEqual "# \t\xe2\x80\xa2\tPortions relating to GIF animations copyright 2004 Jaakko Hyvätti (jaakko.hyvatti@iki.fi)", getline(21) - -Execute(Use :CPYwriteToggleMode to switch off verbatim mode); - call vader#log('===== Calling :CPYwriteToggleMode =====') - CPYwriteToggleMode - -Then(Assert that :CPYwriteToggleMode changes verbatim mode setting); - AssertEqual 0, g:cpywrite#verbatim_mode diff --git a/test/vader/pyx/test_c_langs.vader b/test/vader/test_c_langs.vader similarity index 100% rename from test/vader/pyx/test_c_langs.vader rename to test/vader/test_c_langs.vader diff --git a/test/vader/pyx/test_copyright_template_regexes.vader b/test/vader/test_copyright_template_regexes.vader similarity index 88% rename from test/vader/pyx/test_copyright_template_regexes.vader rename to test/vader/test_copyright_template_regexes.vader index e2136e5..66a9f2f 100644 --- a/test/vader/pyx/test_copyright_template_regexes.vader +++ b/test/vader/test_copyright_template_regexes.vader @@ -110,6 +110,18 @@ Then(Properties quotes the entire 0BSD); AssertEqual '# Permission to use, copy, modify, and/or distribute this software for any purpose', getline(6) AssertEqual '# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH', getline(9) +Execute(Prepend the GD license verbatim to a Julia script); + new! verbatim.jl + b verbatim.jl + CPYwrite GD + +Then(Julia script preserves original third-party copyrights); + AssertEqual '# verbatim.jl', getline(2) + AssertEqual 1, len(getline(3)), 'Allow no trailing spaces' + AssertEqual '# Credits and license terms', getline(6) + AssertEqual "# \t\xe2\x80\xa2\tPortions copyright 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004 by Boutell.Com, Inc.", getline(11) + AssertEqual "# \t\xe2\x80\xa2\tPortions relating to GIF animations copyright 2004 Jaakko Hyvätti (jaakko.hyvatti@iki.fi)", getline(21) + Execute(Use :CPYwriteToggleMode to switch off verbatim mode); call vader#log('===== Calling :CPYwriteToggleMode =====') CPYwriteToggleMode diff --git a/test/vader/pyx/test_functional_langs.vader b/test/vader/test_functional_langs.vader similarity index 100% rename from test/vader/pyx/test_functional_langs.vader rename to test/vader/test_functional_langs.vader diff --git a/test/vader/pyx/test_java_class_doc.vader b/test/vader/test_java_class_doc.vader similarity index 100% rename from test/vader/pyx/test_java_class_doc.vader rename to test/vader/test_java_class_doc.vader diff --git a/test/vader/pyx/test_machine_readable_tags.vader b/test/vader/test_machine_readable_tags.vader similarity index 100% rename from test/vader/pyx/test_machine_readable_tags.vader rename to test/vader/test_machine_readable_tags.vader diff --git a/test/vader/pyx/test_new_langs.vader b/test/vader/test_new_langs.vader similarity index 100% rename from test/vader/pyx/test_new_langs.vader rename to test/vader/test_new_langs.vader diff --git a/test/vader/pyx/test_no_anonymous_copyrights.vader b/test/vader/test_no_anonymous_copyrights.vader similarity index 100% rename from test/vader/pyx/test_no_anonymous_copyrights.vader rename to test/vader/test_no_anonymous_copyrights.vader diff --git a/test/vader/pyx/test_scripting_langs.vader b/test/vader/test_scripting_langs.vader similarity index 100% rename from test/vader/pyx/test_scripting_langs.vader rename to test/vader/test_scripting_langs.vader diff --git a/test/vader/pyx/test_web_langs.vader b/test/vader/test_web_langs.vader similarity index 100% rename from test/vader/pyx/test_web_langs.vader rename to test/vader/test_web_langs.vader