diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3a21864..d8f288b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,3 +5,7 @@ updates: schedule: interval: weekly open-pull-requests-limit: 10 +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 8eba1e9..06606fa 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -1,7 +1,10 @@ name: build on: push: + branches: ["main"] + tags: ["*"] pull_request: + jobs: tests: name: ${{ matrix.name }} @@ -13,39 +16,61 @@ jobs: - { name: "3.8-ipython8", python: "3.8", tox: py38-ipython8 } - { name: "3.12-ipython8", python: "3.12", tox: py12-ipython8 } steps: - - uses: actions/checkout@v3.1.0 - - uses: actions/setup-python@v4.3.0 + - uses: actions/checkout@v4.0.0 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - - run: python -m pip install --upgrade pip + allow-prereleases: true - run: python -m pip install tox - - run: python -m tox -e${{ matrix.tox }} + - run: python -m tox -e ${{ matrix.tox }} + build: + name: Build package + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install pypa/build + run: python -m pip install build + - name: Build a binary wheel and a source tarball + run: python -m build + - name: Install twine + run: python -m pip install twine + - name: Check build + run: python -m twine check --strict dist/* + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ # this duplicates pre-commit.ci, so only run it on tags # it guarantees that linting is passing prior to a release lint-pre-release: - name: lint + if: startsWith(github.ref, 'refs/tags') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.1.0 - - uses: actions/setup-python@v4.3.0 + - uses: actions/checkout@v4.0.0 + - uses: actions/setup-python@v5 with: python-version: "3.11" - - run: python -m pip install --upgrade pip - run: python -m pip install tox - run: python -m tox -elint - release: - needs: [tests, lint-pre-release] + publish-to-pypi: name: PyPI release - if: startsWith(github.ref, 'refs/tags') + if: startsWith(github.ref, 'refs/tags/') + needs: [build, tests, lint-pre-release] runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/doitlive + permissions: + id-token: write steps: - - uses: actions/checkout@v3.1.0 - - uses: actions/setup-python@v4.3.0 - - name: install requirements - run: python -m pip install build twine - - name: build dists - run: python -m build - - name: check package metadata - run: twine check dist/* - - name: publish - run: twine upload -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} dist/* + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Publish distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f1d0ef..30f7eae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,19 +1,14 @@ repos: -- repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.14 hooks: - - id: pyupgrade - args: [--py38-plus] -- repo: https://github.com/psf/black - rev: 23.12.1 + - id: ruff + - id: ruff-format +- repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.27.3 hooks: - - id: black - language_version: python3 -- repo: https://github.com/pycqa/flake8 - rev: 7.0.0 - hooks: - - id: flake8 - additional_dependencies: [flake8-bugbear==23.12.2] + - id: check-github-workflows + - id: check-readthedocs - repo: https://github.com/asottile/blacken-docs rev: 1.16.0 hooks: diff --git a/LICENSE b/LICENSE index 2e4225a..b20df7c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2014-2024 Steven Loria and contributors +Copyright Steven Loria and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index b7f3648..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include *.rst LICENSE \ No newline at end of file diff --git a/RELEASING.rst b/RELEASING.rst index e37c8dc..ab7cc52 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -2,8 +2,8 @@ Releasing ========= -* Bump version in ``doitlive/__version__.py``. -* Update ``CHANGELOG.rst``. +* Bump version in ``pyproject.toml``. +* Update ``CHANGELOG.rst`` with today's date. * Commit: ``git add . && git commit -m "Bump version and update changelog"`` * Tag: ``git tag x.y.z`` * Push (this will trigger a release to PyPI): ``git push --tags origin dev`` diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py index 0dcf53b..64e2499 100755 --- a/docs/_themes/flask_theme_support.py +++ b/docs/_themes/flask_theme_support.py @@ -1,18 +1,18 @@ # flasky extensions. flasky pygments style based on tango style from pygments.style import Style from pygments.token import ( - Keyword, - Name, Comment, - String, Error, + Generic, + Keyword, + Literal, + Name, Number, Operator, - Generic, - Whitespace, - Punctuation, Other, - Literal, + Punctuation, + String, + Whitespace, ) diff --git a/docs/conf.py b/docs/conf.py index e93a7ff..27968aa 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,9 +1,6 @@ -import sys +import importlib.metadata import os -import datetime as dt - -sys.path.insert(0, os.path.abspath("..")) -import doitlive # noqa: E402 +import sys sys.path.append(os.path.abspath("_themes")) @@ -20,9 +17,9 @@ # General information about the project. project = "doitlive" -copyright = f"2014-{dt.datetime.utcnow():%Y}" +copyright = "Steven Loria and contributors" -version = release = doitlive.__version__ +version = release = importlib.metadata.version("doitlive") exclude_patterns = ["_build"] pygments_style = "flask_theme_support.FlaskyStyle" diff --git a/doitlive/__version__.py b/doitlive/__version__.py deleted file mode 100644 index ba7be38..0000000 --- a/doitlive/__version__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "5.0.0" diff --git a/pyproject.toml b/pyproject.toml index 4c6215c..d1f4731 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,60 @@ -[tool.black] -line-length = 88 -target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] +[project] +name = "doitlive" +version = "5.0.0" +description = "Because sometimes you need to do it live." +readme = "README.rst" +license = { file = "LICENSE" } +authors = [{ name = "Steven Loria", email = "sloria1@gmail.com" }] +classifiers = [ + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Environment :: Console", +] +keywords = ["doitlive", "cli", "live", "coding", "presentations", "shell"] +requires-python = ">=3.8" +dependencies = [ + "click>=8.0,<9", + "click-completion>=0.3.1", + "click-didyoumean>=0.0.3", +] + +[project.urls] +Issues = "https://github.com/sloria/doitlive/issues" +Source = "https://github.com/sloria/doitlive/" + +[project.optional-dependencies] +tests = ["pytest", "IPython>=8"] +docs = ["sphinx==7.2.6", "sphinx-issues==4.0.0"] +dev = ["doitlive[tests]", "tox", "pre-commit~=3.6"] + +[build-system] +requires = ["flit_core<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.sdist] +include = ["tests/", "CONTRIBUTING.md", "tox.ini"] +exclude = ["docs/_build/"] + +[tool.ruff] +src = ["src"] +fix = true +show-fixes = true +show-source = true + +[tool.ruff.lint] +ignore = ["E203", "E266", "E501", "E731"] +select = [ + "B", # flake8-bugbear + "E", # pycodestyle error + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "W", # pycodestyle warning +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2ae997a..0000000 --- a/setup.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[metadata] -# This includes the license file in the wheel. -license_files = LICENSE - -[flake8] -max-line-length = 90 -max-complexity = 18 -extend-ignore = E203, E266, E501, E731, B903 - -[tool:pytest] -addopts = -s diff --git a/setup.py b/setup.py deleted file mode 100644 index d85a77b..0000000 --- a/setup.py +++ /dev/null @@ -1,85 +0,0 @@ -import codecs -import re - -from setuptools import setup - -INSTALL_REQUIRES = [ - "click>=8.0,<9", - "click-completion>=0.3.1", - "click-didyoumean>=0.0.3", -] - -EXTRAS_REQUIRE = { - "tests": ["pytest", "IPython"], - "lint": [ - "flake8==7.0.0", - "flake8-bugbear==23.12.2", - "pre-commit~=3.5", - ], - "docs": [ - "sphinx==7.2.6", - "sphinx-issues==4.0.0", - ], -} -EXTRAS_REQUIRE["dev"] = ( - EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + EXTRAS_REQUIRE["docs"] + ["tox"] -) - - -def find_version(fname): - """Attempts to find the version number in the file names fname. - Raises RuntimeError if not found. - """ - version = "" - with codecs.open(fname, "r", encoding="utf-8") as fp: - reg = re.compile(r'__version__ = [\'"]([^\'"]*)[\'"]') - for line in fp: - m = reg.match(line) - if m: - version = m.group(1) - break - if not version: - raise RuntimeError("Cannot find version information") - return version - - -def read(fname): - with codecs.open(fname, "r", encoding="utf-8") as fp: - content = fp.read() - return content - - -setup( - name="doitlive", - version=find_version("doitlive/__version__.py"), - description="Because sometimes you need to do it live.", - long_description=read("README.rst"), - author="Steven Loria", - author_email="sloria1@gmail.com", - url="https://github.com/sloria/doitlive", - install_requires=INSTALL_REQUIRES, - extras_require=EXTRAS_REQUIRE, - python_requires=">=3.8", - license="MIT", - zip_safe=False, - keywords="doitlive cli live coding presentations shell", - classifiers=[ - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Natural Language :: English", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Environment :: Console", - ], - packages=["doitlive"], - entry_points={"console_scripts": ["doitlive = doitlive.cli:cli"]}, - tests_require=["pytest"], - project_urls={ - "Bug Reports": "https://github.com/sloria/doitlive/issues", - "Source": "https://github.com/sloria/doitlive/", - }, -) diff --git a/doitlive/__init__.py b/src/doitlive/__init__.py similarity index 65% rename from doitlive/__init__.py rename to src/doitlive/__init__.py index 38283dc..a572fa8 100644 --- a/doitlive/__init__.py +++ b/src/doitlive/__init__.py @@ -5,20 +5,16 @@ A tool for "live" presentations in the terminal. """ from .cli import SessionState -from .python_consoles import PythonRecorderConsole, PythonPlayerConsole -from .styling import THEMES, Style, TermString, TTY, echo, echo_prompt, format_prompt -from .keyboard import wait_for, magictype, magicrun, ESC, BACKSPACE, CTRLC, RETURNS +from .exceptions import ConfigurationError, DoItLiveError, SessionError +from .keyboard import BACKSPACE, CTRLC, ESC, RETURNS, magicrun, magictype, wait_for +from .python_consoles import PythonPlayerConsole, PythonRecorderConsole +from .styling import THEMES, TTY, Style, TermString, echo, echo_prompt, format_prompt from .version_control import ( get_current_git_branch, get_current_hg_branch, get_current_vcs_branch, ) -from .exceptions import DoItLiveError, ConfigurationError, SessionError -from .__version__ import __version__ -__author__ = "Steven Loria" -__license__ = "MIT" -__version__ = __version__ __all__ = [ "SessionState", "PythonRecorderConsole", diff --git a/doitlive/cli.py b/src/doitlive/cli.py similarity index 96% rename from doitlive/cli.py rename to src/doitlive/cli.py index 556b2e7..4affded 100755 --- a/doitlive/cli.py +++ b/src/doitlive/cli.py @@ -1,4 +1,5 @@ import functools +import importlib.metadata import os import re import shlex @@ -11,7 +12,6 @@ from click import secho, style from click_didyoumean import DYMGroup -from doitlive.__version__ import __version__ from doitlive.exceptions import SessionError from doitlive.keyboard import ( RETURNS, @@ -208,10 +208,10 @@ def run( while more: # slurp up all the python code try: py_command = commands[i].rstrip() - except IndexError: + except IndexError as error: raise SessionError( - "Unmatched {} code block in " "session file.".format(shell_name) - ) + f"Unmatched {shell_name} code block in " "session file." + ) from error i += 1 if py_command.startswith("```"): i += 1 @@ -248,7 +248,7 @@ def run( # ####### -@click.version_option(__version__, "--version", "-v") +@click.version_option(importlib.metadata.version("doitlive"), "--version", "-v") @click.group(cls=DYMGroup, context_settings={"help_option_names": ("-h", "--help")}) def cli(): """doitlive: A tool for "live" presentations in the terminal @@ -328,8 +328,9 @@ def completion(): ) else: echo( - "Please ensure that the {SHELL} environment " - "variable is set.".format(SHELL=style("SHELL", bold=True)) + "Please ensure that the {SHELL} environment " "variable is set.".format( + SHELL=style("SHELL", bold=True) + ) ) sys.exit(1) @@ -520,9 +521,7 @@ def print_recorder_instructions(): ) ) echo( - "To view this help message again, enter {}.".format( - style(HELP_COMMANDS[0], bold=True) - ) + f"To view this help message again, enter {style(HELP_COMMANDS[0], bold=True)}." ) echo() diff --git a/doitlive/exceptions.py b/src/doitlive/exceptions.py similarity index 100% rename from doitlive/exceptions.py rename to src/doitlive/exceptions.py diff --git a/doitlive/ipython/__init__.py b/src/doitlive/ipython/__init__.py similarity index 84% rename from doitlive/ipython/__init__.py rename to src/doitlive/ipython/__init__.py index 6be0752..437db04 100644 --- a/doitlive/ipython/__init__.py +++ b/src/doitlive/ipython/__init__.py @@ -1,8 +1,8 @@ def start_ipython_player(commands, speed): try: from doitlive.ipython.app import PlayerTerminalIPythonApp - except ImportError: - raise RuntimeError("ipython blocks require IPython to be installed") + except ImportError as error: + raise RuntimeError("ipython blocks require IPython to be installed") from error PlayerTerminalIPythonApp.commands = commands PlayerTerminalIPythonApp.speed = speed diff --git a/doitlive/ipython/app.py b/src/doitlive/ipython/app.py similarity index 99% rename from doitlive/ipython/app.py rename to src/doitlive/ipython/app.py index 50a49ff..7836231 100644 --- a/doitlive/ipython/app.py +++ b/src/doitlive/ipython/app.py @@ -5,11 +5,10 @@ from click import Abort from IPython.terminal.interactiveshell import TerminalInteractiveShell from IPython.terminal.ipapp import TerminalIPythonApp - from prompt_toolkit.key_binding import KeyPress from prompt_toolkit.keys import Keys -from doitlive import RETURNS, wait_for, echo +from doitlive import RETURNS, echo, wait_for class DoitliveKeyProcessor: diff --git a/doitlive/keyboard.py b/src/doitlive/keyboard.py similarity index 100% rename from doitlive/keyboard.py rename to src/doitlive/keyboard.py diff --git a/doitlive/python_consoles.py b/src/doitlive/python_consoles.py similarity index 94% rename from doitlive/python_consoles.py rename to src/doitlive/python_consoles.py index 4de580a..1d02c94 100644 --- a/doitlive/python_consoles.py +++ b/src/doitlive/python_consoles.py @@ -1,11 +1,11 @@ """InteractiveConsole subclasses for recording and playback of canned Python statements. """ -from code import InteractiveConsole import sys +from code import InteractiveConsole +from doitlive.keyboard import RETURNS, magictype, wait_for from doitlive.styling import echo_prompt -from doitlive.keyboard import magictype, wait_for, RETURNS class PythonPlayerConsole(InteractiveConsole): @@ -43,11 +43,11 @@ def run_commands(self): def interact(self, banner=None): """Run an interactive session.""" try: - sys.ps1 + sys.ps1 # noqa: B018 except AttributeError: sys.ps1 = ">>>" try: - sys.ps2 + sys.ps2 # noqa: B018 except AttributeError: sys.ps2 = "... " cprt = ( diff --git a/doitlive/styling.py b/src/doitlive/styling.py similarity index 99% rename from doitlive/styling.py rename to src/doitlive/styling.py index 7b99dc0..d8a97d8 100644 --- a/doitlive/styling.py +++ b/src/doitlive/styling.py @@ -186,8 +186,8 @@ def _branch_to_term_string(branch_string): def format_prompt(prompt): try: return prompt.format(**get_prompt_state()) - except KeyError: - raise ConfigurationError("Invalid variable in prompt template.") + except KeyError as error: + raise ConfigurationError("Invalid variable in prompt template.") from error def echo( diff --git a/doitlive/termutils.py b/src/doitlive/termutils.py similarity index 100% rename from doitlive/termutils.py rename to src/doitlive/termutils.py index 86c3f78..5d6e4b8 100644 --- a/doitlive/termutils.py +++ b/src/doitlive/termutils.py @@ -27,8 +27,8 @@ def raw_mode(): yield # needed for the empty context manager to work else: # imports are placed here because this will fail under Windows - import tty import termios + import tty if not isatty(sys.stdin): f = open("/dev/tty") diff --git a/doitlive/version_control.py b/src/doitlive/version_control.py similarity index 100% rename from doitlive/version_control.py rename to src/doitlive/version_control.py index 9bbd820..82ada63 100644 --- a/doitlive/version_control.py +++ b/src/doitlive/version_control.py @@ -1,8 +1,8 @@ """Utility functions to get information about the git or mercurial repository in the working directory """ -import subprocess import os +import subprocess def get_current_git_branch(): diff --git a/tests/test_doitlive.py b/tests/test_doitlive.py index b7e793b..af76b09 100644 --- a/tests/test_doitlive.py +++ b/tests/test_doitlive.py @@ -1,15 +1,14 @@ +import getpass +import importlib.metadata import os import random -import getpass -from contextlib import contextmanager import subprocess +from contextlib import contextmanager import pytest import doitlive from doitlive.cli import cli -from doitlive.__version__ import __version__ - # Check if git is installed git_available = None @@ -210,7 +209,7 @@ def test_completion_fails_if_SHELL_is_unset(runner, monkeypatch): def test_version(runner): result = runner.invoke(cli, ["--version"]) - assert __version__ in result.output + assert importlib.metadata.version("doitlive") in result.output result2 = runner.invoke(cli, ["-v"]) assert result.output == result2.output diff --git a/tests/test_ipython.py b/tests/test_ipython.py index fb9dbca..8078ed5 100644 --- a/tests/test_ipython.py +++ b/tests/test_ipython.py @@ -1,4 +1,5 @@ import os + import click import pytest from prompt_toolkit.key_binding import KeyPress diff --git a/tests/test_styling.py b/tests/test_styling.py index e42b211..2499942 100644 --- a/tests/test_styling.py +++ b/tests/test_styling.py @@ -2,7 +2,7 @@ import pytest from click import style -from doitlive import TermString, TTY +from doitlive import TTY, TermString class TestTermString: