diff --git a/.gitattributes b/.gitattributes index 7de958975..a05e4184d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,11 +6,11 @@ # archive files. See http://git-scm.com/docs/gitattributes for details. /docs/ export-ignore +/scripts/ export-ignore /tests/ export-ignore /.codecov.yml export-ignore /.coveragerc export-ignore /.gitattributes export-ignore /.gitignore export-ignore -/.python-version export-ignore /.travis.yml export-ignore /setup.cfg export-ignore diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml index cdb3baad4..4fe005c8d 100644 --- a/.github/workflows/flake8.yml +++ b/.github/workflows/flake8.yml @@ -24,6 +24,6 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: pip install "flake8<6.0.0" - uses: TrueBrain/actions-flake8@v2 diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 8bbef4334..6a27762d7 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -14,9 +14,9 @@ jobs: platform: ['linux', 'darwin', 'win32'] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' - name: Install mypy diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index d81dc9d08..1e3b26340 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -14,11 +14,11 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.7 - uses: actions/setup-python@v1 + - uses: actions/checkout@v4 + - name: Set up Python 3.8 + uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: 3.8 - name: Update pip and install sphinx run: | python -m pip install --upgrade pip @@ -31,11 +31,11 @@ jobs: check-links: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.7 - uses: actions/setup-python@v1 + - uses: actions/checkout@v4 + - name: Set up Python 3.8 + uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: 3.8 - name: Update pip and install sphinx run: | python -m pip install --upgrade pip diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 68d500b77..31f9700ac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,9 +15,9 @@ jobs: strategy: fail-fast: false matrix: - st-version: [3, 4] + st-version: [4] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: SublimeText/UnitTesting/actions/setup@v1 with: sublime-text-version: ${{ matrix.st-version }} @@ -26,12 +26,12 @@ jobs: run-syntax-tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: SublimeText/UnitTesting/actions/setup@v1 - uses: SublimeText/UnitTesting/actions/run-syntax-tests@v1 check-upgrade-messages: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: kaste/upgrade-messages-test-action@v1 diff --git a/.gitignore b/.gitignore index 56443d9af..b01fc2b98 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ _build/ .mypy_cache/ .ropeproject/ docs-out +scripts/sublime_linter.sublime-package diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..cc1923a40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.8 diff --git a/README.md b/README.md index d961161e7..a0ed12e69 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,13 @@ No linters included: get them via [Package Control](https://packagecontrol.io/se ## Installation +Note: *** We're in a transition phase to the newer ST4 plugin host. Unless we have +more experience for the process, it _may_ be necessary to restart Sublime Text +after installing or upgrading helper packages. Just check if everything works +or if the console shows permanent errors. On my machine, no restarts were +necessary. *** + + Probably don't get fancy and just install SublimeLinter via [Package Control](https://packagecontrol.io/search/SublimeLinter). Refer https://www.sublimelinter.com/en/latest/installation.html for further information, but, spoiler!, diff --git a/__allo__.py b/__allo__.py new file mode 100644 index 000000000..1534014a4 --- /dev/null +++ b/__allo__.py @@ -0,0 +1,141 @@ +from pathlib import Path +import zipfile + +import sublime + +from typing import Set + + +pp = Path(sublime.packages_path()) +ipp = Path(sublime.installed_packages_path()) + + +def ip__has_python_version_file(package: Path) -> bool: + with zipfile.ZipFile(package) as zfile: + try: + zfile.getinfo(".python-version") + except KeyError: + return False + else: + return True + + +def p__has_python_version_file(package: str) -> bool: + fpath = pp / package + return (fpath / ".python-version").exists() + + +def p__is_lift(package: str) -> bool: + fpath = pp / package + return ( + (fpath / ".python-version").exists() + and len(list(fpath.glob("*"))) == 1 + ) + + +def create_python_version_file(package: str) -> None: + fpath = pp / package + fpath.mkdir(exist_ok=True) + (fpath / ".python-version").write_text("3.8\n") + + +def remove_python_version_file(package: str) -> None: + fpath = pp / package + (fpath / ".python-version").unlink(missing_ok=True) + if not list(fpath.glob("*")): + fpath.rmdir() + + +def check_all_plugins() -> None: + removals = [ + (remove_python_version_file, path.stem) + for path in ipp.glob("SublimeLinter*") + if ( + ip__has_python_version_file(path) + and p__is_lift(path.stem) + ) + ] + [ + (remove_python_version_file, path.name) + for path in pp.glob("SublimeLinter*") + if ( + p__is_lift(path.name) + and not (ipp / f"{path.name}.sublime-package").exists() + ) + ] + additions = sorted( + [ + (create_python_version_file, path.stem) + for path in ipp.glob("SublimeLinter*") + if ( + not ip__has_python_version_file(path) + and not p__has_python_version_file(path.stem) + ) + ] + [ + (create_python_version_file, path.name) + for path in pp.glob("SublimeLinter*") + if not p__has_python_version_file(path.name) + ], + key=lambda x: x[1] + ) + + tasks = removals + additions + for fn, package in tasks: + print(f'SublimeLinter-lift: {fn.__name__}("{package}"), ', end="") + try: + fn(package) + except Exception as e: + print(e) + else: + print("ok.") + + if additions: + print("SublimeLinter-lift: If in doubt, reload. 😐") + + +check_all_plugins() # <== side-effect on module load! 🕺 + + +PACKAGE_CONTROL_PREFERENCES_FILE = 'Package Control.sublime-settings' +OBSERVER_KEY = '302e8c92-64a9-4483-b7a7-3a04d2ee641d' +INSTALLED_PLUGINS = set() + + +def package_control_settings() -> sublime.Settings: + return sublime.load_settings(PACKAGE_CONTROL_PREFERENCES_FILE) + + +def plugin_loaded() -> None: + global INSTALLED_PLUGINS + package_control_settings().add_on_change(OBSERVER_KEY, on_change) + INSTALLED_PLUGINS = installed_sl_plugins() + + +def plugin_unloaded() -> None: + package_control_settings().clear_on_change(OBSERVER_KEY) + + +def on_change() -> None: + global INSTALLED_PLUGINS + previous_state, next_state = INSTALLED_PLUGINS, installed_sl_plugins() + additions = next_state - previous_state + deletions = previous_state - next_state + + # We're pessimistic here and assume every plugin needs the lift + # because we want to be early and before PC has actually installed + # the package. + # We call `check_all_plugins` unconditionally which will clean up + # for us if this step was in fact unnecessary. + for package in additions: + create_python_version_file(package) + + if additions or deletions: + sublime.set_timeout(check_all_plugins, 5000) + + INSTALLED_PLUGINS = next_state + + +def installed_sl_plugins() -> Set[str]: + return set( + p for p in package_control_settings().get('installed_packages', []) # type: ignore[union-attr] # stub error + if p.startswith("SublimeLinter-") + ) diff --git a/goto_commands.py b/goto_commands.py index 103935832..20fe1810a 100644 --- a/goto_commands.py +++ b/goto_commands.py @@ -77,7 +77,7 @@ def before_current_pos(pos): move_to(view, point) -class _sublime_linter_move_cursor(sublime_plugin.TextCommand): +class sublime_linter_move_cursor(sublime_plugin.TextCommand): # We ensure `on_selection_modified` handlers run by using a `TextCommand`. # See: https://github.com/SublimeLinter/SublimeLinter/pull/867 # and https://github.com/SublimeTextIssues/Core/issues/485#issuecomment-337480388 @@ -90,7 +90,7 @@ def run(self, edit, point): def move_to(view, point): # type: (sublime.View, int) -> None add_selection_to_jump_history(view) - view.run_command('_sublime_linter_move_cursor', {'point': point}) + view.run_command('sublime_linter_move_cursor', {'point': point}) if int(sublime.version()) < 4000: diff --git a/messages.json b/messages.json index f593141fa..56866cdec 100644 --- a/messages.json +++ b/messages.json @@ -4,5 +4,6 @@ "4.17.0": "messages/4.17.0.txt", "4.19.0": "messages/4.19.0.txt", "4.20.0": "messages/4.20.0.txt", - "4.22.0": "messages/4.22.0.txt" + "4.22.0": "messages/4.22.0.txt", + "4.24.0": "messages/4.24.0.txt" } diff --git a/messages/4.24.0.txt b/messages/4.24.0.txt new file mode 100644 index 000000000..8a7b39412 --- /dev/null +++ b/messages/4.24.0.txt @@ -0,0 +1,23 @@ +SublimeLinter 4.24.0 + +That's it: we say Goodbye to Sublime Text 3 and run on the Python 3.8 host. +You may want to restart. + +How is this implemented? On Sublime's startup and when you install/remove +packages using Package Control we automatically lift all SL adapters/addons +to the 3.8 host. This is necessary as it is impossible to get cooperation +in a single step and limited timeline for 100+ repos. Over time these helper +packages may eventually update and Sublime Linter will remove the "lifts". + +During that period it may or may not be necessary to restart Sublime Text +after upgrades of such plugins. This is currently unknown as I (and we) don't +have much experience in these processes. This is a novel approach after all. +That being said, on my machine installing and uninstalling is seamless and +just works™️. + + +Sincerely, +💕 + + +Yes, I do enjoy coffee: https://paypal.me/herrkaste diff --git a/messages/install.txt b/messages/install.txt index cea57867b..c7067024d 100644 --- a/messages/install.txt +++ b/messages/install.txt @@ -11,6 +11,12 @@ Welcome to SublimeLinter, a linter framework for Sublime Text. Linters are not included, they must be installed separately. Get them from Package Control: https://packagecontrol.io/search/SublimeLinter +*** We're in a transition phase to the newer ST4 plugin host. Unless we have +more experience for the process, it _may_ be necessary to restart Sublime Text +after installing or upgrading helper packages. Just check if everything works +or if the console shows permanent errors. On my machine, no restarts were +necessary. *** + For complete documentation on how to use and configure SublimeLinter, please see: http://www.sublimelinter.com diff --git a/panel_view.py b/panel_view.py index 681ec8507..03e08bee7 100644 --- a/panel_view.py +++ b/panel_view.py @@ -693,10 +693,10 @@ def update_panel_selection(active_view, cursor, draw_info=None, **kwargs): def update_panel_content(panel, text): if not text: text = NO_RESULTS_MESSAGE - panel.run_command('_sublime_linter_replace_panel_content', {'text': text}) + panel.run_command('sublime_linter_replace_panel_content', {'text': text}) -class _sublime_linter_replace_panel_content(sublime_plugin.TextCommand): +class sublime_linter_replace_panel_content(sublime_plugin.TextCommand): def run(self, edit, text): view = self.view _, y = view.viewport_position() @@ -777,10 +777,10 @@ def scroll_into_view(panel, wanted_lines, errors): def scroll_to_line(view, line, animate): """Scroll y-axis so that `line` appears at the top of the viewport.""" x, y = view.text_to_layout(view.text_point(line, 0)) - view.run_command('_sublime_linter_scroll_y', {'y': y, 'animate': animate}) + view.run_command('sublime_linter_scroll_y', {'y': y, 'animate': animate}) -class _sublime_linter_scroll_y(sublime_plugin.TextCommand): +class sublime_linter_scroll_y(sublime_plugin.TextCommand): def run(self, edit, y, animate): x, _ = self.view.viewport_position() self.view.set_viewport_position((x, y), animate) diff --git a/requirements-dev.lock b/requirements-dev.lock index 8a92c1843..6792270c8 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -7,24 +7,11 @@ # all-features: false # with-sources: false -alabaster==0.7.16 - # via sphinx -babel==2.14.0 - # via sphinx -certifi==2024.2.2 - # via requests -charset-normalizer==3.3.2 - # via requests colorama==0.4.6 - # via sphinx # via sphinx-autobuild docutils==0.20.1 # via sphinx flake8==5.0.4 -idna==3.6 - # via requests -imagesize==1.4.1 - # via sphinx jinja2==3.1.3 # via sphinx livereload==2.6.3 @@ -36,38 +23,20 @@ mccabe==0.7.0 mypy==1.8.0 mypy-extensions==1.0.0 # via mypy -packaging==23.2 - # via sphinx pycodestyle==2.9.1 # via flake8 pyflakes==2.5.0 # via flake8 pygments==2.17.2 # via sphinx -requests==2.31.0 - # via sphinx six==1.16.0 # via livereload -snowballstemmer==2.2.0 - # via sphinx -sphinx==7.2.6 +sphinx==1.2.3 # via sphinx-autobuild -sphinx-autobuild==2024.2.4 -sphinxcontrib-applehelp==1.0.8 - # via sphinx -sphinxcontrib-devhelp==1.0.6 - # via sphinx -sphinxcontrib-htmlhelp==2.0.5 - # via sphinx -sphinxcontrib-jsmath==1.0.1 - # via sphinx -sphinxcontrib-qthelp==1.0.7 - # via sphinx -sphinxcontrib-serializinghtml==1.1.10 - # via sphinx +sphinx-autobuild==2021.3.14 +tomli==2.0.1 + # via mypy tornado==6.4 # via livereload typing-extensions==4.10.0 # via mypy -urllib3==2.2.1 - # via requests diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 000000000..6fff1d8c4 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,40 @@ +Test Package Control updates locally + +The main process is also described (here)[https://github.com/SublimeLinter/SublimeLinter/wiki/Test-upgrade-locally]. + +What you have to do is, edit your PC settings, and add *this* folders `repo.json` as an additional +repo: + +```json +{ + "auto_upgrade_frequency": 0, + "bootstrapped": true, + "in_process_packages": + [ + ], + "installed_packages": + [ + "Package Control", + "SublimeLinter", + "SublimeLinter-eslint", + "SublimeLinter-flake8" + ], + "repositories": + [ + "\\repo.json" + ] +} +``` + +The script `update_tester.py` assumes that your current working dir is the root of SublimeLinter. +(Its defaults assume that, you could of course pass all the arguments you want.) + +On the cli you basically just execute + +``` +python scripts/update_tester.py +``` + +This should create the package, bump the version in "repo.json", and start serving it +using a simple local http server. You can then ask PC to upgrade all packages and it +will see and install the new SL version. diff --git a/scripts/repo.json b/scripts/repo.json new file mode 100644 index 000000000..d96f1c85b --- /dev/null +++ b/scripts/repo.json @@ -0,0 +1,20 @@ +{ + "schema_version": "2.0", + "packages": [ + { + "name": "SublimeLinter", + "description": "A full-featured linter framework", + "author": "et.al.", + "homepage": "https://sublimelinter.io", + "donate": "https://sublimelinter.io/about", + "releases": [ + { + "sublime_text": "*", + "version": "21.2.2", + "url": "http://localhost:8000/sublime_linter.sublime-package", + "date": "2024-04-15 13:02:46" + } + ] + } + ] +} \ No newline at end of file diff --git a/scripts/update_tester.py b/scripts/update_tester.py new file mode 100644 index 000000000..8ca3e5075 --- /dev/null +++ b/scripts/update_tester.py @@ -0,0 +1,105 @@ +import argparse +import json +import os +from pathlib import Path +import subprocess +from datetime import datetime + + +def bump(args): + # Read the manifest file + with open(args.manifest, 'r') as manifest_file: + manifest_data = json.load(manifest_file) + + if args.to_version is not None: + new_version = args.to_version + else: + current_version = list(map(int, manifest_data['packages'][0]['releases'][0]['version'].split("."))) + new_version = ".".join(map(str, (current_version[0] + 1, *current_version[1:]))) + + # Update the release version in the packages section + for package in manifest_data.get('packages', []): + for release in package.get('releases', []): + release['version'] = new_version + release['date'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Save the updated manifest + with open(args.manifest, 'w') as manifest_file: + json.dump(manifest_data, manifest_file, indent=2) + + print(f"Manifest version bumped to {new_version}") + + +def serve(args): + print("Running the server, ctrl+c to abort.") + try: + command = "python -m http.server 8000" + subprocess.run(command, cwd=args.dir, shell=True) + except KeyboardInterrupt: + print("\nServer terminated by user.") + + +def pack(args): + # cwd = os.path.abspath(args.repo) + target = os.path.abspath(args.file) + command = ("git", "archive", "--format=zip", "-o", target, "HEAD") + subprocess.run(command, cwd=args.repo, shell=True) + + +def main(): + this_directory = Path(__file__).parent + parser = argparse.ArgumentParser(description="Update and serve local packages.") + parser.add_argument("--to-version", help="Version to bump to") + parser.add_argument( + "--manifest", type=str, default=this_directory / "repo.json", help="Manifest file" + ) + parser.add_argument( + "--repo", type=str, default=".", help="Base folder of the repo you want to archive" + ) + parser.add_argument( + "dir", nargs="?", default=this_directory, help="Directory to serve" + ) + parser.add_argument( + "file", nargs="?", default=this_directory / "sublime_linter.sublime-package", help="Name of the package file" + ) + + subparsers = parser.add_subparsers(title="subcommands", dest="subcommand") + + # Bump subcommand + bump_parser = subparsers.add_parser("bump", help="Bump version") + bump_parser.add_argument("--to-version", help="Version to bump to") + bump_parser.add_argument( + "--manifest", type=str, default=this_directory / "repo.json", help="Manifest file" + ) + + # Serve subcommand + serve_parser = subparsers.add_parser("serve", help="Serve directory") + serve_parser.add_argument( + "dir", nargs="?", default=this_directory, help="Directory to serve" + ) + + # Archive subcommand + pack_parser = subparsers.add_parser("pack", help="Create package file") + pack_parser.add_argument( + "--repo", type=str, default=".", help="Base folder of the repo you want to archive" + ) + pack_parser.add_argument( + "file", nargs="?", default=this_directory / "sublime_linter.sublime-package", help="Name of the package file" + ) + + args = parser.parse_args() + + if args.subcommand == "bump": + bump(args) + elif args.subcommand == "serve": + serve(args) + elif args.subcommand == "pack": + pack(args) + else: + pack(args) + bump(args) + serve(args) + + +if __name__ == "__main__": + main() diff --git a/tests/test_filter_results.py b/tests/test_filter_results.py index 4091cbd4f..0a21b0897 100644 --- a/tests/test_filter_results.py +++ b/tests/test_filter_results.py @@ -13,6 +13,7 @@ from unittesting import DeferrableTestCase from SublimeLinter.tests.parameterized import parameterized as p from SublimeLinter.tests.mockito import ( + contains, unstub, verify, when, @@ -120,7 +121,7 @@ class FakeLinter(Linter): self.assertEqual(result, expected) @p.expand([ - (['d('], "'d(' in 'filter_errors' is not a valid regex pattern: 'unbalanced parenthesis'."), + (['d('], contains("'d(' in 'filter_errors' is not a valid regex pattern")), (True, "'filter_errors' must be set to a string or a list of strings.\nGot 'True' instead"), (123, "'filter_errors' must be set to a string or a list of strings.\nGot '123' instead"), ]) diff --git a/tests/test_linter_validity.py b/tests/test_linter_validity.py index 39d555f22..d24366a94 100644 --- a/tests/test_linter_validity.py +++ b/tests/test_linter_validity.py @@ -299,7 +299,7 @@ class Fake(Linter): self.assertTrue(linter.disabled) verify(linter_module.logger).error( - contains("error compiling regex: unbalanced parenthesis.") + contains("error compiling regex") ) def test_valid_and_registered_without_defining_regex(self): diff --git a/tests/test_util.py b/tests/test_util.py index 85fd42f20..c8a0eda1b 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -38,7 +38,7 @@ def test_emits_nicely_formatted_warning(self): verify(util.logger).warning("""\ Executing `python --foo` failed - Command '['python', '--foo']' returned non-zero exit status 2 + Command '['python', '--foo']' returned non-zero exit status 2. ... unknown option --foo unknown option --foo